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  
19  import org.apache.commons.lang.Validate;
20  import org.slf4j.Logger;
21  import org.slf4j.LoggerFactory;
22  
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.TreeMap;
29  import java.util.Collections;
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
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          Validate.notNull(pluginFactories, "The list of plugin factories must be specified");
71          Validate.notNull(pluginEventManager, "The event manager must be specified");
72          Validate.notNull(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 Collection<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 Collections.unmodifiableCollection(new ArrayList<Plugin>(plugins.values()));
100     }
101 
102     protected Plugin deployPluginFromUnit(final DeploymentUnit deploymentUnit, final ModuleDescriptorFactory moduleDescriptorFactory)
103     {
104         Plugin plugin = null;
105         String errorText = "No plugin factories found for plugin file " + deploymentUnit;
106 
107         String pluginKey = null;
108         for (final PluginFactory factory : pluginFactories)
109         {
110             try
111             {
112                 final PluginArtifact artifact = pluginArtifactFactory.create(deploymentUnit.getPath().toURI());
113                 pluginKey = factory.canCreate(artifact);
114                 if (pluginKey != null)
115                 {
116                     plugin = factory.create(artifact, moduleDescriptorFactory);
117                     if (plugin != null)
118                     {
119                         break;
120                     }
121                 }
122             }
123             catch (final Throwable ex)
124             {
125                 log.error("Unable to deploy plugin '" + pluginKey + "', file " + deploymentUnit, ex);
126                 errorText = ex.getMessage();
127                 break;
128             }
129         }
130         if (plugin == null)
131         {
132             plugin = new UnloadablePlugin(errorText);
133             if (pluginKey != null)
134             {
135                 plugin.setKey(pluginKey);
136             }
137             else
138             {
139                 plugin.setKey(deploymentUnit.getPath().getName());
140             }
141 
142         }
143         else
144         {
145             log.info("Plugin " + deploymentUnit + " created");
146         }
147 
148         return plugin;
149     }
150 
151     public boolean supportsRemoval()
152     {
153         return true;
154     }
155 
156     public boolean supportsAddition()
157     {
158         return true;
159     }
160 
161     /**
162      * @return all plugins, now loaded by the pluginLoader, which have been discovered and added since the
163      * last time a check was performed.
164      */
165     public Collection<Plugin> addFoundPlugins(final ModuleDescriptorFactory moduleDescriptorFactory) throws PluginParseException
166     {
167         // find missing plugins
168         final Collection<DeploymentUnit> updatedDeploymentUnits = scanner.scan();
169 
170         // create list while updating internal state
171         final List<Plugin> foundPlugins = new ArrayList<Plugin>();
172         for (final DeploymentUnit deploymentUnit : updatedDeploymentUnits)
173         {
174             if (!plugins.containsKey(deploymentUnit))
175             {
176                 Plugin plugin = deployPluginFromUnit(deploymentUnit, moduleDescriptorFactory);
177                 plugin = postProcess(plugin);
178                 plugins.put(deploymentUnit, plugin);
179                 foundPlugins.add(plugin);
180             }
181         }
182         if (foundPlugins.isEmpty())
183         {
184             log.info("No plugins found to be installed");
185         }
186 
187         return foundPlugins;
188     }
189 
190     /**
191      * @param plugin - the plugin to remove
192      * @throws com.atlassian.plugin.PluginException representing the reason for failure.
193      */
194     public void removePlugin(final Plugin plugin) throws PluginException
195     {
196         if (plugin.getPluginState() == PluginState.ENABLED)
197         {
198             throw new PluginException("Cannot remove an enabled plugin");
199         }
200 
201         if (!plugin.isUninstallable())
202         {
203             throw new PluginException("Cannot remove an uninstallable plugin: [" + plugin.getName() + "]");
204         }
205 
206         final DeploymentUnit deploymentUnit = findMatchingDeploymentUnit(plugin);
207         plugin.uninstall();
208 
209         try
210         {
211             // Loop over to see if there are any other deployment units with the same filename. This will happen
212             // if a newer plugin is uploaded with the same filename as the plugin being removed: in this case the
213             // old one has already been deleted
214             boolean found = false;
215             for (final DeploymentUnit unit : plugins.keySet())
216             {
217                 if (unit.getPath().equals(deploymentUnit.getPath()) && !unit.equals(deploymentUnit))
218                 {
219                     found = true;
220                     break;
221                 }
222             }
223 
224             if (!found)
225             {
226                 scanner.remove(deploymentUnit);
227             }
228         }
229         catch (final SecurityException e)
230         {
231             throw new PluginException(e);
232         }
233 
234         plugins.remove(deploymentUnit);
235         log.info("Removed plugin " + plugin.getKey());
236     }
237 
238     private DeploymentUnit findMatchingDeploymentUnit(final Plugin plugin) throws PluginException
239     {
240         DeploymentUnit deploymentUnit = null;
241         for (final Map.Entry<DeploymentUnit, Plugin> entry : plugins.entrySet())
242         {
243             // no, you don't want to use entry.getValue().equals(plugin) here as it breaks upgrades where it is a new
244             // version of the plugin but the key and version number hasn't changed, and hence, equals() will always return
245             // true
246             if (entry.getValue() == plugin)
247             {
248                 deploymentUnit = entry.getKey();
249                 break;
250             }
251         }
252 
253         if (deploymentUnit == null)
254         {
255             throw new PluginException("This pluginLoader has no memory of deploying the plugin you are trying remove: [" + plugin.getName() + "]");
256         }
257         return deploymentUnit;
258     }
259 
260     /**
261      * Called during plugin framework shutdown
262      * @param event The shutdown event
263      */
264     @PluginEventListener
265     public void onShutdown(final PluginFrameworkShutdownEvent event)
266     {
267         for (final Iterator<Plugin> it = plugins.values().iterator(); it.hasNext();)
268         {
269             final Plugin plugin = it.next();
270             if (plugin.isUninstallable())
271             {
272                 plugin.uninstall();
273             }
274             it.remove();
275         }
276 
277         scanner.reset();
278     }
279 
280     /**
281      * @deprecated Since 2.0.0, shutdown will automatically occur when the plugin framework is shutdown
282      */
283     @Deprecated
284     public void shutDown()
285     {
286         onShutdown(null);
287     }
288 
289     /**
290      * Determines if the artifact can be loaded by any of its deployers
291      *
292      * @param pluginArtifact The artifact to test
293      * @return True if this artifact can be loaded by this loader
294      * @throws com.atlassian.plugin.PluginParseException
295      */
296     public String canLoad(final PluginArtifact pluginArtifact) throws PluginParseException
297     {
298         String pluginKey = null;
299         for (final PluginFactory factory : pluginFactories)
300         {
301             pluginKey = factory.canCreate(pluginArtifact);
302             if (pluginKey != null)
303             {
304                 break;
305             }
306         }
307         return pluginKey;
308     }
309 
310     /**
311      * Template method that can be used by a specific {@link PluginLoader} to
312      * add information to a {@link Plugin} after it has been loaded.
313      *
314      * @param plugin a plugin that has been loaded
315      * @since v2.2.0
316      */
317     protected Plugin postProcess(final Plugin plugin)
318     {
319         return plugin;
320     }
321 }