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