View Javadoc

1   package com.atlassian.plugin.osgi.factory;
2   
3   import com.atlassian.plugin.AutowireCapablePlugin;
4   import com.atlassian.plugin.IllegalPluginStateException;
5   import com.atlassian.plugin.InstallationMode;
6   import com.atlassian.plugin.ModuleDescriptor;
7   import com.atlassian.plugin.PluginArtifact;
8   import com.atlassian.plugin.PluginState;
9   import com.atlassian.plugin.event.PluginEventListener;
10  import com.atlassian.plugin.event.PluginEventManager;
11  import com.atlassian.plugin.event.events.PluginContainerFailedEvent;
12  import com.atlassian.plugin.event.events.PluginContainerRefreshedEvent;
13  import com.atlassian.plugin.event.events.PluginFrameworkShutdownEvent;
14  import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
15  import com.atlassian.plugin.event.events.PluginRefreshedEvent;
16  import com.atlassian.plugin.impl.AbstractPlugin;
17  import com.atlassian.plugin.module.ContainerAccessor;
18  import com.atlassian.plugin.module.ContainerManagedPlugin;
19  import com.atlassian.plugin.osgi.container.OsgiContainerException;
20  import com.atlassian.plugin.osgi.container.OsgiContainerManager;
21  import com.atlassian.plugin.osgi.event.PluginServiceDependencyWaitEndedEvent;
22  import com.atlassian.plugin.osgi.event.PluginServiceDependencyWaitStartingEvent;
23  import com.atlassian.plugin.osgi.event.PluginServiceDependencyWaitTimedOutEvent;
24  import com.atlassian.plugin.osgi.external.ListableModuleDescriptorFactory;
25  import com.atlassian.plugin.osgi.util.OsgiSystemBundleUtil;
26  import com.atlassian.plugin.util.PluginUtils;
27  import com.google.common.annotations.VisibleForTesting;
28  import org.dom4j.Element;
29  import org.osgi.framework.Bundle;
30  import org.osgi.framework.BundleContext;
31  import org.osgi.framework.BundleEvent;
32  import org.osgi.framework.BundleException;
33  import org.osgi.framework.BundleListener;
34  import org.osgi.framework.ServiceReference;
35  import org.osgi.framework.SynchronousBundleListener;
36  import org.osgi.service.packageadmin.PackageAdmin;
37  import org.osgi.util.tracker.ServiceTracker;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  import java.io.InputStream;
42  import java.net.URL;
43  import java.util.HashMap;
44  import java.util.Map;
45  import java.util.Set;
46  import java.util.concurrent.CopyOnWriteArraySet;
47  
48  import static com.google.common.base.Preconditions.checkNotNull;
49  
50  /**
51   * Plugin that wraps an OSGi bundle that does contain a plugin descriptor.  The actual bundle is not created until the
52   * {@link #install()} method is invoked.  Any attempt to access a method that requires a bundle will throw an
53   * {@link com.atlassian.plugin.IllegalPluginStateException}.
54   *
55   * This class uses a {@link OsgiPluginHelper} to represent different behaviors of key methods in different states.
56   * {@link OsgiPluginUninstalledHelper} implements the methods when the plugin hasn't yet been installed into the
57   * OSGi container, while {@link OsgiPluginInstalledHelper} implements the methods when the bundle is available.  This
58   * leaves this class to manage the {@link PluginState} and interactions with the event system.
59   */
60  //@Threadsafe
61  public class OsgiPlugin extends AbstractPlugin implements AutowireCapablePlugin, ContainerManagedPlugin
62  {
63      /**
64       * Manifest key for the Atlassian plugin key entry
65       */
66      public static final String ATLASSIAN_PLUGIN_KEY = "Atlassian-Plugin-Key";
67  
68      /**
69       * Manifest key denoting Atlassian remote plugins
70       */
71      public static final String REMOTE_PLUGIN_KEY = "Remote-Plugin";
72  
73      private final Map<String, Element> moduleElements = new HashMap<String, Element>();
74      private final PluginEventManager pluginEventManager;
75      private final PackageAdmin packageAdmin;
76      private final Set<OutstandingDependency> outstandingDependencies = new CopyOnWriteArraySet<OutstandingDependency>();
77      private final Logger log = LoggerFactory.getLogger(this.getClass());
78      private final BundleListener bundleStartStopListener;
79      private final PluginArtifact originalPluginArtifact;
80  
81      private volatile boolean treatPluginContainerCreationAsRefresh = false;
82      private volatile OsgiPluginHelper helper;
83  
84      // Until the framework is actually done starting we want to ignore @RequiresRestart. Where this comes into play
85      // is when we have one version of a plugin (e.g. via bundled-plugins.zip) installed but then discover a newer
86      // one in installed-plugins. Clearly we can't "require a restart" between those two stages. And since nothing has
87      // been published outside of plugins yet (and thus can't be cached by the host app) the @RequiresRestart is
88      // meaningless.
89      private volatile boolean frameworkStarted = false;
90  
91      public OsgiPlugin(final String key, final OsgiContainerManager mgr, final PluginArtifact artifact, final PluginArtifact originalPluginArtifact, final PluginEventManager pluginEventManager)
92      {
93          this.originalPluginArtifact = checkNotNull(originalPluginArtifact);
94          this.pluginEventManager = checkNotNull(pluginEventManager);
95  
96          this.helper = new OsgiPluginUninstalledHelper(
97                  checkNotNull(key, "The plugin key is required"),
98                  checkNotNull(mgr, "The osgi container is required"),
99                  checkNotNull(artifact, "The plugin artifact is required"));
100         this.packageAdmin = extractPackageAdminFromOsgi(mgr);
101 
102         this.bundleStartStopListener = new SynchronousBundleListener()
103         {
104             public void bundleChanged(final BundleEvent bundleEvent)
105             {
106                 if (bundleEvent.getBundle() == getBundle())
107                 {
108                     if (bundleEvent.getType() == BundleEvent.STOPPING)
109                     {
110                         helper.onDisable();
111                         setPluginState(PluginState.DISABLED);
112                     }
113                     else if (bundleEvent.getType() == BundleEvent.STARTED)
114                     {
115                         BundleContext ctx = getBundle().getBundleContext();
116                         helper.onEnable(createServiceTrackers(ctx));
117                         setPluginState(PluginState.ENABLED);
118                     }
119                 }
120             }
121         };
122     }
123 
124     /**
125      * Only used for testing
126      * @param helper The helper to use
127      */
128     @VisibleForTesting
129     OsgiPlugin(PluginEventManager pluginEventManager, OsgiPluginHelper helper)
130     {
131         this.helper = helper;
132         this.pluginEventManager = pluginEventManager;
133         this.packageAdmin = null;
134         this.bundleStartStopListener = null;
135         this.originalPluginArtifact = null;
136     }
137 
138     /**
139      * @return The active bundle
140      * @throws IllegalPluginStateException if the bundle hasn't been created yet
141      */
142     public Bundle getBundle() throws IllegalPluginStateException
143     {
144         return helper.getBundle();
145     }
146 
147     @Override
148     public InstallationMode getInstallationMode()
149     {
150         return helper.isRemotePlugin() ? InstallationMode.REMOTE : InstallationMode.LOCAL;
151     }
152 
153     /**
154      * @return true
155      */
156     public boolean isUninstallable()
157     {
158         return true;
159     }
160 
161     /**
162      * @return true
163      */
164     public boolean isDynamicallyLoaded()
165     {
166         return true;
167     }
168 
169     /**
170      * @return true
171      */
172     public boolean isDeleteable()
173     {
174         return true;
175     }
176 
177     public PluginArtifact getPluginArtifact()
178     {
179         return originalPluginArtifact;
180     }
181 
182     /**
183      * @param clazz The name of the class to be loaded
184      * @param callingClass The class calling the loading (used to help find a classloader)
185      * @param <T> The class type
186      * @return The class instance, loaded from the OSGi bundle
187      * @throws ClassNotFoundException If the class cannot be found
188      * @throws IllegalPluginStateException if the bundle hasn't been created yet
189      */
190     public <T> Class<T> loadClass(final String clazz, final Class<?> callingClass) throws ClassNotFoundException, IllegalPluginStateException
191     {
192         return helper.loadClass(clazz, callingClass);
193     }
194 
195     /**
196      * @param name The resource name
197      * @return The resource URL, null if not found
198      * @throws IllegalPluginStateException if the bundle hasn't been created yet
199      */
200     public URL getResource(final String name) throws IllegalPluginStateException
201     {
202         return helper.getResource(name);
203     }
204 
205     /**
206      * @param name The name of the resource to be loaded.
207      * @return Null if not found
208      * @throws IllegalPluginStateException if the bundle hasn't been created yet
209      */
210     public InputStream getResourceAsStream(final String name) throws IllegalPluginStateException
211     {
212         return helper.getResourceAsStream(name);
213     }
214 
215     /**
216      * @return The classloader to load classes and resources from the bundle
217      * @throws IllegalPluginStateException if the bundle hasn't been created yet
218      */
219     public ClassLoader getClassLoader() throws IllegalPluginStateException
220     {
221         return helper.getClassLoader();
222     }
223 
224     /**
225      * Called when the plugin container for the bundle has failed to be created.  This means the bundle is still
226      * active, but the plugin container is not available, so for our purposes, the plugin shouldn't be enabled.
227      *
228      * @param event The plugin container failed event
229      * @throws com.atlassian.plugin.IllegalPluginStateException If the plugin key hasn't been set yet
230      */
231     @PluginEventListener
232     public void onPluginContainerFailed(final PluginContainerFailedEvent event) throws IllegalPluginStateException
233     {
234         if (getKey() == null)
235         {
236             throw new IllegalPluginStateException("Plugin key must be set");
237         }
238         if (getKey().equals(event.getPluginKey()))
239         {
240             logAndClearOustandingDependencies();
241             // TODO: do something with the exception more than logging
242             getLog().error("Unable to start the plugin container for plugin " + getKey(), event.getCause());
243             setPluginState(PluginState.DISABLED);
244         }
245     }
246 
247     @PluginEventListener
248     public void onPluginFrameworkStartedEvent(final PluginFrameworkStartedEvent event)
249     {
250         frameworkStarted = true;
251     }
252 
253     @PluginEventListener
254     public void onPluginFrameworkShutdownEvent(final PluginFrameworkShutdownEvent event)
255     {
256         frameworkStarted = false;
257     }
258 
259     @PluginEventListener
260     public void onServiceDependencyWaitStarting(PluginServiceDependencyWaitStartingEvent event)
261     {
262         if (event.getPluginKey() != null && event.getPluginKey().equals(getKey()))
263         {
264             OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
265             outstandingDependencies.add(dep);
266             getLog().info(generateOutstandingDependencyLogMessage(dep, "Waiting for"));
267         }
268     }
269 
270     @PluginEventListener
271     public void onServiceDependencyWaitEnded(PluginServiceDependencyWaitEndedEvent event)
272     {
273         if (event.getPluginKey() != null && event.getPluginKey().equals(getKey()))
274         {
275             OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
276             outstandingDependencies.remove(dep);
277             getLog().info(generateOutstandingDependencyLogMessage(dep, "Found"));
278         }
279     }
280 
281     @PluginEventListener
282     public void onServiceDependencyWaitEnded(PluginServiceDependencyWaitTimedOutEvent event)
283     {
284         if (event.getPluginKey() != null && event.getPluginKey().equals(getKey()))
285         {
286             OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
287             outstandingDependencies.remove(dep);
288             getLog().error(generateOutstandingDependencyLogMessage(dep, "Timeout waiting for "));
289         }
290     }
291 
292     private String generateOutstandingDependencyLogMessage(OutstandingDependency dep, String action)
293     {
294         StringBuilder sb = new StringBuilder();
295         sb.append(action).append(" ");
296         sb.append("service '").append(dep.getBeanName()).append("' for plugin '").append(getKey()).append("' with filter ").append(dep.getFilter());
297         return sb.toString();
298     }
299 
300     /**
301      * Called when the plugin container for the bundle has been created or refreshed.  If this is the first time the
302      * context has been refreshed, then it is a new context.  Otherwise, this means that the bundle has been reloaded,
303      * usually due to a dependency upgrade.
304      *
305      * @param event The event
306      * @throws com.atlassian.plugin.IllegalPluginStateException If the plugin key hasn't been set yet
307      */
308     @PluginEventListener
309     public void onPluginContainerRefresh(final PluginContainerRefreshedEvent event) throws IllegalPluginStateException
310     {
311         if (getKey() == null)
312         {
313             throw new IllegalPluginStateException("Plugin key must be set");
314         }
315         if (getKey().equals(event.getPluginKey()))
316         {
317             outstandingDependencies.clear();
318             helper.setPluginContainer(event.getContainer());
319             if (!compareAndSetPluginState(PluginState.ENABLING, PluginState.ENABLED) && getPluginState() != PluginState.ENABLED)
320             {
321                 log.warn("Ignoring the bean container that was just created for plugin " + getKey() + ".  The plugin " +
322                           "is in an invalid state, " + getPluginState() + ", that doesn't support a transition to " +
323                           "enabled.  Most likely, it was disabled due to a timeout.");
324                 helper.setPluginContainer(null);
325                 return;
326             }
327 
328             // Only send refresh event on second creation
329             if (treatPluginContainerCreationAsRefresh)
330             {
331                 pluginEventManager.broadcast(new PluginRefreshedEvent(this));
332             }
333             else
334             {
335                 treatPluginContainerCreationAsRefresh = true;
336             }
337         }
338     }
339 
340     /**
341      * Creates and autowires the class, preferring constructor inject, then falling back to private field and setter
342      *
343      * @throws IllegalPluginStateException if the bundle hasn't been created yet
344      */
345     public <T> T autowire(final Class<T> clazz) throws IllegalPluginStateException
346     {
347         return autowire(clazz, AutowireStrategy.AUTOWIRE_AUTODETECT);
348     }
349 
350     /**
351      * Creates and autowires the class
352      *
353      * @throws IllegalPluginStateException if the bundle hasn't been created yet
354      */
355     public <T> T autowire(final Class<T> clazz, final AutowireStrategy autowireStrategy) throws IllegalPluginStateException
356     {
357         return helper.getRequiredContainerAccessor().createBean(clazz);
358     }
359 
360     /**
361      * Autowires the instance using plugin container's default injection algorithm
362      *
363      * @throws IllegalPluginStateException if the bundle hasn't been created yet
364      */
365     public void autowire(final Object instance) throws IllegalStateException
366     {
367         autowire(instance, AutowireStrategy.AUTOWIRE_AUTODETECT);
368     }
369 
370     /**
371      * Autowires the instance
372      *
373      * @throws IllegalPluginStateException if the bundle hasn't been created yet
374      */
375     public void autowire(final Object instance, final AutowireStrategy autowireStrategy) throws IllegalPluginStateException
376     {
377         helper.getRequiredContainerAccessor().injectBean(instance);
378     }
379 
380     /**
381      * Determines which plugins are required for this one to operate based on tracing the "wires" or packages that
382      * are imported by this plugin.  Bundles that provide those packages are determined to be required plugins.
383      *
384      * @return A set of bundle symbolic names, or plugin keys.  Empty set if none.
385      * @since 2.2.0
386      */
387     @Override
388     public Set<String> getRequiredPlugins() throws IllegalPluginStateException
389     {
390         return helper.getRequiredPlugins();
391     }
392 
393     @Override
394     public String toString()
395     {
396         return getKey();
397     }
398 
399     /**
400      * Installs the plugin artifact into OSGi
401      *
402      * @throws IllegalPluginStateException if the bundle hasn't been created yet
403      */
404     @Override
405     protected void installInternal() throws IllegalPluginStateException
406     {
407         log.debug("Installing OSGi plugin '{}'", getKey());
408         Bundle bundle = helper.install();
409         helper = new OsgiPluginInstalledHelper(bundle, packageAdmin);
410     }
411 
412     /**
413      * Enables the plugin by setting the OSGi bundle state to enabled.
414      *
415      * @return {@link PluginState#ENABLED}if the container is being refreshed or {@link PluginState#ENABLING} if we are waiting
416      * on a plugin container (first time being activated, as all subsequent times are considered refreshes)
417      * @throws OsgiContainerException If the underlying OSGi system threw an exception or we tried to enable the bundle
418      * when it was in an invalid state
419      * @throws IllegalPluginStateException if the bundle hasn't been created yet
420      */
421     @Override
422     protected synchronized PluginState enableInternal() throws OsgiContainerException, IllegalPluginStateException
423     {
424         log.debug("Enabling OSGi plugin '{}'", getKey());
425         PluginState stateResult;
426         try
427         {
428             if (getBundle().getState() == Bundle.ACTIVE)
429             {
430                 log.debug("Plugin '{}' bundle is already active, not doing anything", getKey());
431                 stateResult = PluginState.ENABLED;
432             }
433             else if ((getBundle().getState() == Bundle.RESOLVED) || (getBundle().getState() == Bundle.INSTALLED))
434             {
435                 pluginEventManager.register(this);
436                 if (!treatPluginContainerCreationAsRefresh)
437                 {
438                     stateResult = PluginState.ENABLING;
439                     // Set it immediately, since the plugin container refresh event could happen at any time
440                     setPluginState(stateResult);
441                 }
442                 else
443                 {
444                     stateResult = PluginState.ENABLED;
445                 }
446                 log.debug("Plugin '{}' bundle is resolved or installed, starting.", getKey());
447                 getBundle().start();
448                 final BundleContext ctx = getBundle().getBundleContext();
449                 helper.onEnable(createServiceTrackers(ctx));
450 
451                 // ensure the bean factory is removed when the bundle is stopped
452                 OsgiSystemBundleUtil.getSystemBundleContext(ctx).addBundleListener(bundleStartStopListener);
453             }
454             else
455             {
456                 throw new OsgiContainerException("Cannot enable the plugin '" + getKey() + "' when the bundle is not in the resolved or installed state: "
457                         + getBundle().getState() + "(" + getBundle().getBundleId() + ")");
458             }
459 
460             // Only set state to enabling if it hasn't already been enabled via another thread notifying of a plugin
461             // container creation during the execution of this method
462             return (getPluginState() != PluginState.ENABLED ? stateResult : PluginState.ENABLED);
463         }
464         catch (final BundleException e)
465         {
466             log.error("Detected an error (BundleException) enabling the plugin '" + getKey() + "' : " + e.getMessage() + ". " +
467                       " This error usually occurs when your plugin imports a package from another bundle with a specific version constraint " +
468                     "and either the bundle providing that package doesn't meet those version constraints, or there is no bundle " +
469                     "available that provides the specified package. For more details on how to fix this, see " +
470                     "https://developer.atlassian.com/x/mQAN");
471             throw new OsgiContainerException("Cannot start plugin: " + getKey(), e);
472         }
473     }
474 
475     private ServiceTracker[] createServiceTrackers(BundleContext ctx)
476     {
477         return new ServiceTracker[] {
478                 new ServiceTracker(ctx, ModuleDescriptor.class.getName(),
479                         new ModuleDescriptorServiceTrackerCustomizer(this, pluginEventManager)),
480                 new ServiceTracker(ctx, ListableModuleDescriptorFactory.class.getName(),
481                         new UnrecognizedModuleDescriptorServiceTrackerCustomizer(this, pluginEventManager))
482         };
483     }
484 
485     /**
486      * Disables the plugin by changing the bundle state back to resolved
487      *
488      * @throws OsgiContainerException If the OSGi system threw an exception
489      * @throws IllegalPluginStateException if the bundle hasn't been created yet
490      */
491     @Override
492     protected synchronized void disableInternal() throws OsgiContainerException, IllegalPluginStateException
493     {
494         // Only disable underlying bundle if this is a truly dynamic plugin
495         if (!requiresRestart())
496         {
497             try
498             {
499                 if (getPluginState() == PluginState.DISABLING)
500                 {
501                     logAndClearOustandingDependencies();
502                 }
503                 helper.onDisable();
504                 pluginEventManager.unregister(this);
505                 OsgiSystemBundleUtil.getSystemBundleContext(getBundle()).removeBundleListener(bundleStartStopListener);
506                 getBundle().stop();
507                 treatPluginContainerCreationAsRefresh = false;
508             }
509             catch (final BundleException e)
510             {
511                 throw new OsgiContainerException("Cannot stop plugin: " + getKey(), e);
512             }
513         }
514     }
515 
516     private boolean requiresRestart()
517     {
518         return frameworkStarted && PluginUtils.doesPluginRequireRestart(this);
519     }
520 
521     private void logAndClearOustandingDependencies()
522     {
523         for (OutstandingDependency dep : outstandingDependencies)
524         {
525             getLog().error(generateOutstandingDependencyLogMessage(dep, "Never resolved"));
526         }
527         outstandingDependencies.clear();
528     }
529 
530     /**
531      * Uninstalls the bundle from the OSGi container
532      * @throws OsgiContainerException If the underlying OSGi system threw an exception
533      * @throws IllegalPluginStateException if the bundle hasn't been created yet
534      */
535     @Override
536     protected void uninstallInternal() throws OsgiContainerException, IllegalPluginStateException
537     {
538         try
539         {
540             if (getBundle().getState() != Bundle.UNINSTALLED)
541             {
542                 pluginEventManager.unregister(this);
543                 getBundle().uninstall();
544                 helper.onUninstall();
545                 setPluginState(PluginState.UNINSTALLED);
546             }
547         }
548         catch (final BundleException e)
549         {
550             throw new OsgiContainerException("Cannot uninstall bundle " + getBundle().getSymbolicName());
551         }
552     }
553 
554     /**
555      * Adds a module descriptor XML element for later processing, needed for dynamic module support
556      *
557      * @param key The module key
558      * @param element The module element
559      */
560     void addModuleDescriptorElement(final String key, final Element element)
561     {
562         moduleElements.put(key, element);
563     }
564 
565     /**
566      * Exposes {@link #removeModuleDescriptor(String)} for package-protected classes
567      *
568      * @param key The module descriptor key
569      */
570     void clearModuleDescriptor(String key)
571     {
572         removeModuleDescriptor(key);
573     }
574 
575     /**
576      * Gets the module elements for dynamic module descriptor handling.  Doesn't need to return a copy or anything
577      * immutable because it is only accessed by package-private helper classes
578      *
579      * @return The map of module keys to module XML elements
580      */
581     Map<String, Element> getModuleElements()
582     {
583         return moduleElements;
584     }
585 
586     /**
587      * Extracts the {@link PackageAdmin} instance from the OSGi container
588      * @param mgr The OSGi container manager
589      * @return The package admin instance, should never be null
590      */
591     private PackageAdmin extractPackageAdminFromOsgi(OsgiContainerManager mgr)
592     {
593         // Get the system bundle (always bundle 0)
594         Bundle bundle = mgr.getBundles()[0];
595 
596         // We assume the package admin will always be available
597         final ServiceReference ref = bundle.getBundleContext()
598                 .getServiceReference(PackageAdmin.class.getName());
599         return (PackageAdmin) bundle.getBundleContext()
600                 .getService(ref);
601     }
602 
603     public ContainerAccessor getContainerAccessor()
604     {
605         return helper.getContainerAccessor();
606     }
607 
608     private static class OutstandingDependency
609     {
610         private final String beanName;
611         private final String filter;
612 
613         public OutstandingDependency(String beanName, String filter)
614         {
615             this.beanName = beanName;
616             this.filter = filter;
617         }
618 
619         public String getBeanName()
620         {
621             return beanName;
622         }
623 
624         public String getFilter()
625         {
626             return filter;
627         }
628 
629         @Override
630         public boolean equals(Object o)
631         {
632             if (this == o)
633             {
634                 return true;
635             }
636             if (o == null || getClass() != o.getClass())
637             {
638                 return false;
639             }
640 
641             OutstandingDependency that = (OutstandingDependency) o;
642 
643             if (beanName != null ? !beanName.equals(that.beanName) : that.beanName != null)
644             {
645                 return false;
646             }
647             if (!filter.equals(that.filter))
648             {
649                 return false;
650             }
651 
652             return true;
653         }
654 
655         @Override
656         public int hashCode()
657         {
658             int result = beanName != null ? beanName.hashCode() : 0;
659             result = 31 * result + filter.hashCode();
660             return result;
661         }
662     }
663 }