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