View Javadoc

1   package com.atlassian.plugin.loaders;
2   
3   import com.atlassian.plugin.DefaultPluginArtifactFactory;
4   import com.atlassian.plugin.ModuleDescriptorFactory;
5   import com.atlassian.plugin.Plugin;
6   import com.atlassian.plugin.PluginArtifact;
7   import com.atlassian.plugin.PluginArtifactFactory;
8   import com.atlassian.plugin.PluginException;
9   import com.atlassian.plugin.PluginParseException;
10  import com.atlassian.plugin.PluginState;
11  import com.atlassian.plugin.event.PluginEventListener;
12  import com.atlassian.plugin.event.PluginEventManager;
13  import com.atlassian.plugin.event.events.PluginFrameworkShutdownEvent;
14  import com.atlassian.plugin.factories.PluginFactory;
15  import com.atlassian.plugin.impl.UnloadablePlugin;
16  import com.atlassian.plugin.loaders.classloading.DeploymentUnit;
17  import com.atlassian.plugin.loaders.classloading.Scanner;
18  import com.google.common.collect.ImmutableList;
19  import org.slf4j.Logger;
20  import org.slf4j.LoggerFactory;
21  
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.TreeMap;
28  
29  import static com.google.common.base.Preconditions.checkNotNull;
30  
31  /**
32   * Plugin loader that delegates the detection of plugins to a Scanner instance. The scanner may monitor the contents
33   * of a directory on disk, a database, or any other place plugins may be hidden.
34   *
35   * @since 2.1.0
36   */
37  public class ScanningPluginLoader implements DynamicPluginLoader, DiscardablePluginLoader
38  {
39      private static final Logger log = LoggerFactory.getLogger(ScanningPluginLoader.class);
40  
41      protected final com.atlassian.plugin.loaders.classloading.Scanner scanner;
42      protected final Map<DeploymentUnit, Plugin> plugins;
43      protected final List<PluginFactory> pluginFactories;
44      protected final PluginArtifactFactory pluginArtifactFactory;
45  
46      /**
47       * Constructor that provides a default plugin artifact factory
48       *                                                                                     `
49       * @param scanner The scanner to use to detect new plugins
50       * @param pluginFactories The deployers that will handle turning an artifact into a plugin
51       * @param pluginEventManager The event manager, used for listening for shutdown events
52       * @since 2.0.0
53       */
54      public ScanningPluginLoader(final Scanner scanner, final List<PluginFactory> pluginFactories, final PluginEventManager pluginEventManager)
55      {
56          this(scanner, pluginFactories, new DefaultPluginArtifactFactory(), pluginEventManager);
57      }
58  
59      /**
60       * Construct a new scanning plugin loader with no default values
61       *
62       * @param scanner The scanner to use to detect new plugins
63       * @param pluginFactories The deployers that will handle turning an artifact into a plugin
64       * @param pluginArtifactFactory used to create new plugin artifacts from an URL
65       * @param pluginEventManager The event manager, used for listening for shutdown events
66       * @since 2.0.0
67       */
68      public ScanningPluginLoader(final Scanner scanner, final List<PluginFactory> pluginFactories, final PluginArtifactFactory pluginArtifactFactory, final PluginEventManager pluginEventManager)
69      {
70          checkNotNull(pluginFactories, "The list of plugin factories must be specified");
71          checkNotNull(pluginEventManager, "The event manager must be specified");
72          checkNotNull(scanner, "The scanner must be specified");
73  
74          plugins = new TreeMap<DeploymentUnit, Plugin>();
75  
76          this.pluginArtifactFactory = pluginArtifactFactory;
77          this.scanner = scanner;
78          this.pluginFactories = new ArrayList<PluginFactory>(pluginFactories);
79  
80          pluginEventManager.register(this);
81      }
82  
83      public Iterable<Plugin> loadAllPlugins(final ModuleDescriptorFactory moduleDescriptorFactory)
84      {
85          scanner.scan();
86  
87          for (final DeploymentUnit deploymentUnit : scanner.getDeploymentUnits())
88          {
89              Plugin plugin = deployPluginFromUnit(deploymentUnit, moduleDescriptorFactory);
90              plugin = postProcess(plugin);
91              plugins.put(deploymentUnit, plugin);
92          }
93  
94          if (scanner.getDeploymentUnits().isEmpty())
95          {
96              log.info("No plugins found to be deployed");
97          }
98  
99          return ImmutableList.copyOf(plugins.values());
100     }
101 
102     /**
103      * @return all plugins, now loaded by the pluginLoader, which have been discovered and added since the
104      * last time a check was performed.
105      */
106     public Iterable<Plugin> loadFoundPlugins(final ModuleDescriptorFactory moduleDescriptorFactory) throws PluginParseException
107     {
108         // find missing plugins
109         final Collection<DeploymentUnit> updatedDeploymentUnits = scanner.scan();
110 
111         // create list while updating internal state
112         final List<Plugin> foundPlugins = new ArrayList<Plugin>();
113         for (final DeploymentUnit deploymentUnit : updatedDeploymentUnits)
114         {
115             if (!plugins.containsKey(deploymentUnit))
116             {
117                 Plugin plugin = deployPluginFromUnit(deploymentUnit, moduleDescriptorFactory);
118                 plugin = postProcess(plugin);
119                 plugins.put(deploymentUnit, plugin);
120                 foundPlugins.add(plugin);
121             }
122         }
123         if (foundPlugins.isEmpty())
124         {
125             log.info("No plugins found to be installed");
126         }
127 
128         return ImmutableList.copyOf(foundPlugins);
129     }
130 
131     public boolean supportsRemoval()
132     {
133         return true;
134     }
135 
136     public boolean supportsAddition()
137     {
138         return true;
139     }
140 
141     protected final Plugin deployPluginFromUnit(final DeploymentUnit deploymentUnit, final ModuleDescriptorFactory moduleDescriptorFactory)
142     {
143         Plugin plugin = null;
144         String errorText = "No plugin factories found for plugin file " + deploymentUnit;
145 
146         String pluginKey = null;
147         for (final PluginFactory factory : pluginFactories)
148         {
149             try
150             {
151                 final PluginArtifact artifact = pluginArtifactFactory.create(deploymentUnit.getPath().toURI());
152                 pluginKey = factory.canCreate(artifact);
153                 if (pluginKey != null)
154                 {
155                     plugin = factory.create(artifact, moduleDescriptorFactory);
156                     if (plugin != null)
157                     {
158                         log.debug("Plugin factory '{}' created plugin '{}'.", factory.getClass().getName(), pluginKey);
159                         break;
160                     }
161                 }
162             }
163             catch (final Throwable ex)
164             {
165                 log.error("Unable to deploy plugin '{}' from '{}'.", pluginKey, deploymentUnit);
166                 log.error("Because of the following exception:", ex);
167 
168                 errorText = ex.getMessage();
169                 break;
170             }
171         }
172         if (plugin == null)
173         {
174             plugin = new UnloadablePlugin(errorText);
175             if (pluginKey != null)
176             {
177                 plugin.setKey(pluginKey);
178             }
179             else
180             {
181                 plugin.setKey(deploymentUnit.getPath().getName());
182             }
183             log.debug("Could not find a suitable factory for plugin '{}' of '{}'", pluginKey, deploymentUnit);
184         }
185         else
186         {
187             log.debug("Plugin '{}' created from '{}'", plugin.getKey(), deploymentUnit);
188         }
189 
190         return plugin;
191     }
192 
193     /**
194      * @param plugin - the plugin to remove
195      * @throws com.atlassian.plugin.PluginException representing the reason for failure.
196      */
197     public void removePlugin(final Plugin plugin) throws PluginException
198     {
199         if (plugin.getPluginState() == PluginState.ENABLED)
200         {
201             throw new PluginException("Cannot remove enabled plugin '" + plugin.getKey() + '"');
202         }
203         if (!plugin.isUninstallable())
204         {
205             throw new PluginException("Cannot remove uninstallable plugin '" + plugin.getKey() + '"');
206         }
207 
208         final DeploymentUnit deploymentUnit = findMatchingDeploymentUnit(plugin);
209         plugin.uninstall();
210 
211         if (plugin.isDeleteable())
212         {
213             // If this throws (which it does if the file exists, it's directory is writable, and yet deletion fails),
214             // we will leak resources. However, this is not unique to this code, and i'm loathe to change the exception
215             // behaviour here in case there are ramifications in UPM. If this is getting restructured in 4.0 we can
216             // hopefully revisit when we've got a backward compatibility break point.
217             deleteDeploymentUnit(deploymentUnit);
218         }
219 
220         plugins.remove(deploymentUnit);
221         log.info("Removed plugin '" + plugin.getKey() + "'");
222     }
223 
224     private void deleteDeploymentUnit(final DeploymentUnit deploymentUnit)
225     {
226         try
227         {
228             // Loop over to see if there are any other deployment units with the same filename. This will happen
229             // if a newer plugin is uploaded with the same filename as the plugin being removed: in this case the
230             // old one has already been deleted
231             boolean found = false;
232             for (final DeploymentUnit unit : plugins.keySet())
233             {
234                 if (unit.getPath().equals(deploymentUnit.getPath()) && !unit.equals(deploymentUnit))
235                 {
236                     found = true;
237                     break;
238                 }
239             }
240 
241             if (!found)
242             {
243                 scanner.remove(deploymentUnit);
244             }
245         }
246         catch (final SecurityException e)
247         {
248             throw new PluginException(e);
249         }
250     }
251 
252     private DeploymentUnit findMatchingDeploymentUnit(final Plugin plugin) throws PluginException
253     {
254         DeploymentUnit deploymentUnit = null;
255         for (final Map.Entry<DeploymentUnit, Plugin> entry : plugins.entrySet())
256         {
257             // no, you don't want to use entry.getValue().equals(plugin) here as it breaks upgrades where it is a new
258             // version of the plugin but the key and version number hasn't changed, and hence, equals() will always return
259             // true
260             if (entry.getValue() == plugin)
261             {
262                 deploymentUnit = entry.getKey();
263                 break;
264             }
265         }
266 
267         if (deploymentUnit == null)
268         {
269             throw new PluginException("This pluginLoader has no memory of deploying the plugin you are trying remove: [" + plugin.getName() + "]");
270         }
271         return deploymentUnit;
272     }
273 
274     /**
275      * Called during plugin framework shutdown
276      * @param event The shutdown event
277      */
278     @PluginEventListener
279     public void onShutdown(final PluginFrameworkShutdownEvent event)
280     {
281         for (final Iterator<Plugin> it = plugins.values().iterator(); it.hasNext();)
282         {
283             final Plugin plugin = it.next();
284             if (plugin.isUninstallable())
285             {
286                 plugin.uninstall();
287             }
288             it.remove();
289         }
290 
291         scanner.reset();
292     }
293 
294     /**
295      * @deprecated Since 2.0.0, shutdown will automatically occur when the plugin framework is shutdown
296      */
297     @Deprecated
298     public void shutDown()
299     {
300         onShutdown(null);
301     }
302 
303     @Override
304     public boolean isDynamicPluginLoader()
305     {
306         return true;
307     }
308 
309     /**
310      * Determines if the artifact can be loaded by any of its deployers
311      *
312      * @param pluginArtifact The artifact to test
313      * @return True if this artifact can be loaded by this loader
314      * @throws com.atlassian.plugin.PluginParseException
315      */
316     public String canLoad(final PluginArtifact pluginArtifact) throws PluginParseException
317     {
318         String pluginKey = null;
319         for (final PluginFactory factory : pluginFactories)
320         {
321             pluginKey = factory.canCreate(pluginArtifact);
322             if (pluginKey != null)
323             {
324                 break;
325             }
326         }
327         return pluginKey;
328     }
329 
330     @Override
331     public void discardPlugin(final Plugin plugin)
332     {
333         // findMatchingDeploymentUnit throws rather than return null, so we won't remove(null)
334         plugins.remove(findMatchingDeploymentUnit(plugin));
335     }
336 
337     /**
338      * Template method that can be used by a specific {@link PluginLoader} to
339      * add information to a {@link Plugin} after it has been loaded.
340      *
341      * @param plugin a plugin that has been loaded
342      * @since v2.2.0
343      */
344     protected Plugin postProcess(final Plugin plugin)
345     {
346         return plugin;
347     }
348 }