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