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