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.Plugin;
8   import com.atlassian.plugin.PluginArtifact;
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.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.Date;
44  import java.util.HashMap;
45  import java.util.Map;
46  import java.util.Set;
47  import java.util.concurrent.CopyOnWriteArraySet;
48  
49  import static com.google.common.base.Preconditions.checkNotNull;
50  
51  /**
52   * Plugin that wraps an OSGi bundle that does contain a plugin descriptor.  The actual bundle is not created until the
53   * {@link #install()} method is invoked.  Any attempt to access a method that requires a bundle will throw an
54   * {@link com.atlassian.plugin.IllegalPluginStateException}.
55   *
56   * This class uses a {@link OsgiPluginHelper} to represent different behaviors of key methods in different states.
57   * {@link OsgiPluginUninstalledHelper} implements the methods when the plugin hasn't yet been installed into the
58   * OSGi container, while {@link OsgiPluginInstalledHelper} implements the methods when the bundle is available.  This
59   * leaves this class to manage the {@link PluginState} and interactions with the event system.
60   */
61  //@Threadsafe
62  public class OsgiPlugin extends AbstractPlugin implements AutowireCapablePlugin, ContainerManagedPlugin, Plugin.Resolvable
63  {
64      private static final Logger log = LoggerFactory.getLogger(OsgiPlugin.class);
65  
66      /**
67       * Manifest key for the Atlassian plugin key entry
68       */
69      public static final String ATLASSIAN_PLUGIN_KEY = "Atlassian-Plugin-Key";
70  
71      /**
72       * Manifest key for additional scan folders
73       */
74      public static final String ATLASSIAN_SCAN_FOLDERS = "Atlassian-Scan-Folders";
75  
76      /**
77       * Manifest key denoting Atlassian remote plugins
78       */
79      public static final String REMOTE_PLUGIN_KEY = "Remote-Plugin";
80  
81      private final Map<String, Element> moduleElements = new HashMap<String, Element>();
82      private final PluginEventManager pluginEventManager;
83      private final PackageAdmin packageAdmin;
84      private final Set<OutstandingDependency> outstandingDependencies = new CopyOnWriteArraySet<OutstandingDependency>();
85      private final BundleListener bundleStartStopListener;
86      private final PluginArtifact originalPluginArtifact;
87  
88      private volatile boolean treatPluginContainerCreationAsRefresh = false;
89      private volatile OsgiPluginHelper helper;
90  
91      // Until the framework is actually done starting we want to ignore @RequiresRestart. Where this comes into play
92      // is when we have one version of a plugin (e.g. via bundled-plugins.zip) installed but then discover a newer
93      // one in installed-plugins. Clearly we can't "require a restart" between those two stages. And since nothing has
94      // been published outside of plugins yet (and thus can't be cached by the host app) the @RequiresRestart is
95      // meaningless.
96      private volatile boolean frameworkStarted = false;
97  
98      public OsgiPlugin(final String key, final OsgiContainerManager mgr, final PluginArtifact artifact, final PluginArtifact originalPluginArtifact, final PluginEventManager pluginEventManager)
99      {
100         this.originalPluginArtifact = checkNotNull(originalPluginArtifact);
101         this.pluginEventManager = checkNotNull(pluginEventManager);
102 
103         this.helper = new OsgiPluginUninstalledHelper(
104                 checkNotNull(key, "The plugin key is required"),
105                 checkNotNull(mgr, "The osgi container is required"),
106                 checkNotNull(artifact, "The plugin artifact is required"));
107         this.packageAdmin = extractPackageAdminFromOsgi(mgr);
108 
109         this.bundleStartStopListener = new SynchronousBundleListener()
110         {
111             public void bundleChanged(final BundleEvent bundleEvent)
112             {
113                 if (bundleEvent.getBundle() == getBundle())
114                 {
115                     if (bundleEvent.getType() == BundleEvent.STOPPING)
116                     {
117                         helper.onDisable();
118                         setPluginState(PluginState.DISABLED);
119                     }
120                     else if (bundleEvent.getType() == BundleEvent.STARTED)
121                     {
122                         BundleContext ctx = getBundle().getBundleContext();
123                         helper.onEnable(createServiceTrackers(ctx));
124                         setPluginState(PluginState.ENABLED);
125                     }
126                 }
127             }
128         };
129     }
130 
131     /**
132      * Only used for testing.
133      * @param pluginEventManager The PluginEventManager to use.
134      * @param helper The OsgiPluginHelper to use.
135      * @param packageAdmin the PackageAdmin to use.
136      */
137     @VisibleForTesting
138     OsgiPlugin(PluginEventManager pluginEventManager, OsgiPluginHelper helper, PackageAdmin packageAdmin)
139     {
140         this.helper = helper;
141         this.pluginEventManager = pluginEventManager;
142         this.packageAdmin = packageAdmin;
143         this.bundleStartStopListener = null;
144         this.originalPluginArtifact = null;
145     }
146 
147     /**
148      * @return The active bundle
149      * @throws IllegalPluginStateException if the bundle hasn't been created yet
150      */
151     public Bundle getBundle() throws IllegalPluginStateException
152     {
153         return helper.getBundle();
154     }
155 
156     @Override
157     public InstallationMode getInstallationMode()
158     {
159         return helper.isRemotePlugin() ? InstallationMode.REMOTE : InstallationMode.LOCAL;
160     }
161 
162     /**
163      * @return true
164      */
165     public boolean isUninstallable()
166     {
167         return true;
168     }
169 
170     /**
171      * @return true
172      */
173     public boolean isDynamicallyLoaded()
174     {
175         return true;
176     }
177 
178     /**
179      * @return true
180      */
181     public boolean isDeleteable()
182     {
183         return true;
184     }
185 
186 
187     @Override
188     public Date getDateInstalled()
189     {
190         long date = getPluginArtifact().toFile().lastModified();
191         if (date == 0)
192         {
193             date = getDateLoaded().getTime();
194         }
195         return new Date(date);
196     }
197 
198     public PluginArtifact getPluginArtifact()
199     {
200         return originalPluginArtifact;
201     }
202 
203     /**
204      * @param clazz The name of the class to be loaded
205      * @param callingClass The class calling the loading (used to help find a classloader)
206      * @param <T> The class type
207      * @return The class instance, loaded from the OSGi bundle
208      * @throws ClassNotFoundException If the class cannot be found
209      * @throws IllegalPluginStateException if the bundle hasn't been created yet
210      */
211     public <T> Class<T> loadClass(final String clazz, final Class<?> callingClass) throws ClassNotFoundException, IllegalPluginStateException
212     {
213         return helper.loadClass(clazz, callingClass);
214     }
215 
216     /**
217      * @param name The resource name
218      * @return The resource URL, null if not found
219      * @throws IllegalPluginStateException if the bundle hasn't been created yet
220      */
221     public URL getResource(final String name) throws IllegalPluginStateException
222     {
223         return helper.getResource(name);
224     }
225 
226     /**
227      * @param name The name of the resource to be loaded.
228      * @return Null if not found
229      * @throws IllegalPluginStateException if the bundle hasn't been created yet
230      */
231     public InputStream getResourceAsStream(final String name) throws IllegalPluginStateException
232     {
233         return helper.getResourceAsStream(name);
234     }
235 
236     /**
237      * @return The classloader to load classes and resources from the bundle
238      * @throws IllegalPluginStateException if the bundle hasn't been created yet
239      */
240     public ClassLoader getClassLoader() throws IllegalPluginStateException
241     {
242         return helper.getClassLoader();
243     }
244 
245     /**
246      * Called when the plugin container for the bundle has failed to be created.  This means the bundle is still
247      * active, but the plugin container is not available, so for our purposes, the plugin shouldn't be enabled.
248      *
249      * @param event The plugin container failed event
250      * @throws com.atlassian.plugin.IllegalPluginStateException If the plugin key hasn't been set yet
251      */
252     @PluginEventListener
253     public void onPluginContainerFailed(final PluginContainerFailedEvent event) throws IllegalPluginStateException
254     {
255         if (getKey() == null)
256         {
257             throw new IllegalPluginStateException("Plugin key must be set");
258         }
259         if (getKey().equals(event.getPluginKey()))
260         {
261             logAndClearOustandingDependencies();
262             // TODO: do something with the exception more than logging
263             log.error("Unable to start the plugin container for plugin '{}'", getKey(), event.getCause());
264             setPluginState(PluginState.DISABLED);
265         }
266     }
267 
268     @PluginEventListener
269     public void onPluginFrameworkStartedEvent(final PluginFrameworkStartedEvent event)
270     {
271         frameworkStarted = true;
272     }
273 
274     @PluginEventListener
275     public void onPluginFrameworkShutdownEvent(final PluginFrameworkShutdownEvent event)
276     {
277         frameworkStarted = false;
278     }
279 
280     @PluginEventListener
281     public void onServiceDependencyWaitStarting(PluginServiceDependencyWaitStartingEvent event)
282     {
283         if (event.getPluginKey() != null && event.getPluginKey().equals(getKey()))
284         {
285             OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
286             outstandingDependencies.add(dep);
287             log.info("Plugin '{}' waiting for {}", getKey(), dep);
288         }
289     }
290 
291     @PluginEventListener
292     public void onServiceDependencyWaitEnded(PluginServiceDependencyWaitEndedEvent event)
293     {
294         if (event.getPluginKey() != null && event.getPluginKey().equals(getKey()))
295         {
296             OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
297             outstandingDependencies.remove(dep);
298             log.info("Plugin '{}' found {}", getKey(), dep);
299         }
300     }
301 
302     @PluginEventListener
303     public void onServiceDependencyWaitEnded(PluginServiceDependencyWaitTimedOutEvent event)
304     {
305         if (event.getPluginKey() != null && event.getPluginKey().equals(getKey()))
306         {
307             OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
308             outstandingDependencies.remove(dep);
309             log.error("Plugin '{}' timeout waiting for {}", getKey(), dep);
310         }
311     }
312 
313     /**
314      * Called when the plugin container for the bundle has been created or refreshed.  If this is the first time the
315      * context has been refreshed, then it is a new context.  Otherwise, this means that the bundle has been reloaded,
316      * usually due to a dependency upgrade.
317      *
318      * @param event The event
319      * @throws com.atlassian.plugin.IllegalPluginStateException If the plugin key hasn't been set yet
320      */
321     @PluginEventListener
322     public void onPluginContainerRefresh(final PluginContainerRefreshedEvent event) throws IllegalPluginStateException
323     {
324         if (getKey() == null)
325         {
326             throw new IllegalPluginStateException("Plugin key must be set");
327         }
328         if (getKey().equals(event.getPluginKey()))
329         {
330             outstandingDependencies.clear();
331             helper.setPluginContainer(event.getContainer());
332             if (!compareAndSetPluginState(PluginState.ENABLING, PluginState.ENABLED) && getPluginState() != PluginState.ENABLED)
333             {
334                 log.warn("Ignoring the bean container that was just created for plugin " + getKey() + ".  The plugin " +
335                           "is in an invalid state, " + getPluginState() + ", that doesn't support a transition to " +
336                           "enabled.  Most likely, it was disabled due to a timeout.");
337                 helper.setPluginContainer(null);
338                 return;
339             }
340 
341             // Only send refresh event on second creation
342             if (treatPluginContainerCreationAsRefresh)
343             {
344                 pluginEventManager.broadcast(new PluginRefreshedEvent(this));
345             }
346             else
347             {
348                 treatPluginContainerCreationAsRefresh = true;
349             }
350         }
351     }
352 
353     /**
354      * Creates and autowires the class, preferring constructor inject, then falling back to private field and setter
355      *
356      * @throws IllegalPluginStateException if the bundle hasn't been created yet
357      */
358     public <T> T autowire(final Class<T> clazz) throws IllegalPluginStateException
359     {
360         return autowire(clazz, AutowireStrategy.AUTOWIRE_AUTODETECT);
361     }
362 
363     /**
364      * Creates and autowires the class
365      *
366      * @throws IllegalPluginStateException if the bundle hasn't been created yet
367      */
368     public <T> T autowire(final Class<T> clazz, final AutowireStrategy autowireStrategy) throws IllegalPluginStateException
369     {
370         return helper.getRequiredContainerAccessor().createBean(clazz);
371     }
372 
373     /**
374      * Autowires the instance using plugin container's default injection algorithm
375      *
376      * @throws IllegalPluginStateException if the bundle hasn't been created yet
377      */
378     public void autowire(final Object instance) throws IllegalStateException
379     {
380         autowire(instance, AutowireStrategy.AUTOWIRE_AUTODETECT);
381     }
382 
383     /**
384      * Autowires the instance
385      *
386      * @throws IllegalPluginStateException if the bundle hasn't been created yet
387      */
388     public void autowire(final Object instance, final AutowireStrategy autowireStrategy) throws IllegalPluginStateException
389     {
390         helper.getRequiredContainerAccessor().injectBean(instance);
391     }
392 
393     /**
394      * Determines which plugins are required for this one to operate based on tracing the "wires" or packages that
395      * are imported by this plugin.  Bundles that provide those packages are determined to be required plugins.
396      *
397      * @return A set of bundle symbolic names, or plugin keys.  Empty set if none.
398      * @since 2.2.0
399      */
400     @Override
401     public Set<String> getRequiredPlugins() throws IllegalPluginStateException
402     {
403         return helper.getRequiredPlugins();
404     }
405 
406     @Override
407     public String toString()
408     {
409         return getKey();
410     }
411 
412     /**
413      * Installs the plugin artifact into OSGi
414      *
415      * @throws IllegalPluginStateException if the bundle hasn't been created yet
416      */
417     @Override
418     protected void installInternal() throws IllegalPluginStateException
419     {
420         log.debug("Installing OSGi plugin '{}'", getKey());
421         Bundle bundle = helper.install();
422         helper = new OsgiPluginInstalledHelper(bundle, packageAdmin);
423     }
424 
425     /**
426      * Enables the plugin by setting the OSGi bundle state to enabled.
427      *
428      * @return {@link PluginState#ENABLED}if the container is being refreshed or {@link PluginState#ENABLING} if we are waiting
429      * on a plugin container (first time being activated, as all subsequent times are considered refreshes)
430      * @throws OsgiContainerException If the underlying OSGi system threw an exception or we tried to enable the bundle
431      * when it was in an invalid state
432      * @throws IllegalPluginStateException if the bundle hasn't been created yet
433      */
434     @Override
435     protected synchronized PluginState enableInternal() throws OsgiContainerException, IllegalPluginStateException
436     {
437         log.debug("Enabling OSGi plugin '{}'", getKey());
438         PluginState stateResult;
439         try
440         {
441             if (getBundle().getState() == Bundle.ACTIVE)
442             {
443                 log.debug("Plugin '{}' bundle is already active, not doing anything", getKey());
444                 stateResult = PluginState.ENABLED;
445             }
446             else if ((getBundle().getState() == Bundle.RESOLVED) || (getBundle().getState() == Bundle.INSTALLED))
447             {
448                 pluginEventManager.register(this);
449                 if (!treatPluginContainerCreationAsRefresh)
450                 {
451                     // Move to ENABLING before starting the bundle which triggers the plugin container refresh event
452                     setPluginState(PluginState.ENABLING);
453                     // Since we've taken over management of state change, tell our caller not to bother
454                     stateResult = PluginState.PENDING;
455                 }
456                 else
457                 {
458                     stateResult = PluginState.ENABLED;
459                 }
460                 log.debug("Plugin '{}' bundle is resolved or installed, starting.", getKey());
461                 getBundle().start();
462                 final BundleContext ctx = getBundle().getBundleContext();
463                 helper.onEnable(createServiceTrackers(ctx));
464 
465                 // ensure the bean factory is removed when the bundle is stopped
466                 ctx.addBundleListener(bundleStartStopListener);
467             }
468             else
469             {
470                 throw new OsgiContainerException("Cannot enable the plugin '" + getKey() + "' when the bundle is not in the resolved or installed state: "
471                         + getBundle().getState() + "(" + getBundle().getBundleId() + ")");
472             }
473 
474             // Return our desired state, which can be PENDING if the container refresh will complete the enable
475             return stateResult;
476         }
477         catch (final BundleException e)
478         {
479             log.error("Detected an error (BundleException) enabling the plugin '" + getKey() + "' : " + e.getMessage() + ". " +
480                       " This error usually occurs when your plugin imports a package from another bundle with a specific version constraint " +
481                     "and either the bundle providing that package doesn't meet those version constraints, or there is no bundle " +
482                     "available that provides the specified package. For more details on how to fix this, see " +
483                     "https://developer.atlassian.com/x/mQAN");
484             throw new OsgiContainerException("Cannot start plugin: " + getKey(), e);
485         }
486     }
487 
488     private ServiceTracker[] createServiceTrackers(BundleContext ctx)
489     {
490         return new ServiceTracker[] {
491                 new ServiceTracker(ctx, ModuleDescriptor.class.getName(),
492                         new ModuleDescriptorServiceTrackerCustomizer(this, pluginEventManager)),
493                 new ServiceTracker(ctx, ListableModuleDescriptorFactory.class.getName(),
494                         new UnrecognizedModuleDescriptorServiceTrackerCustomizer(this, pluginEventManager))
495         };
496     }
497 
498     /**
499      * Disables the plugin by changing the bundle state back to resolved
500      *
501      * @throws OsgiContainerException If the OSGi system threw an exception
502      * @throws IllegalPluginStateException if the bundle hasn't been created yet
503      */
504     @Override
505     protected synchronized void disableInternal() throws OsgiContainerException, IllegalPluginStateException
506     {
507         // Only disable underlying bundle if this is a truly dynamic plugin
508         if (!requiresRestart())
509         {
510             try
511             {
512                 if (getPluginState() == PluginState.DISABLING)
513                 {
514                     logAndClearOustandingDependencies();
515                 }
516                 helper.onDisable();
517                 pluginEventManager.unregister(this);
518                 getBundle().stop();
519                 treatPluginContainerCreationAsRefresh = false;
520             }
521             catch (final BundleException e)
522             {
523                 log.error("Detected an error (BundleException) disabling the plugin '" + getKey() + "' : " + e.getMessage() + ".");
524                 throw new OsgiContainerException("Cannot stop plugin: " + getKey(), e);
525             }
526         }
527     }
528 
529     private boolean requiresRestart()
530     {
531         return frameworkStarted && PluginUtils.doesPluginRequireRestart(this);
532     }
533 
534     private void logAndClearOustandingDependencies()
535     {
536         for (OutstandingDependency dep : outstandingDependencies)
537         {
538             log.error("Plugin '{}' never resolved {}", getKey(), dep);
539         }
540         outstandingDependencies.clear();
541     }
542 
543     /**
544      * Uninstalls the bundle from the OSGi container
545      * @throws OsgiContainerException If the underlying OSGi system threw an exception
546      * @throws IllegalPluginStateException if the bundle hasn't been created yet
547      */
548     @Override
549     protected void uninstallInternal() throws OsgiContainerException, IllegalPluginStateException
550     {
551         final String key = getKey();
552 
553         int retryCount = 0;
554         Exception rootCause = null;
555         final long sleepTime = 500L;
556 
557         while (true)
558         {
559             try
560             {
561                 // We unregister irrespective of whether the plugin is installed. Since it's idempotent it's safe,
562                 // and if we're trying to uninstall uninstalled things, then stopping more events won't hurt.
563                 pluginEventManager.unregister(this);
564                 final Bundle bundle = getBundleIfInstalled();
565                 if (null != bundle)
566                 {
567                     // This code is being updated not just for better memory performance, but also in response to improving
568                     // diagnostics around some plugin update issues (see PLUG-1084), so it is very defensive.
569                     if (bundle.getState() != Bundle.UNINSTALLED)
570                     {
571                         bundle.uninstall();
572                     }
573                     else
574                     {
575                         log.warn("Bundle for '{}' already UNINSTALLED, but still held by helper '{}'", key, helper);
576                     }
577                     // We want to fire the helper event late as we're being defensive
578                     final OsgiPluginHelper oldHelper = helper;
579                     helper = new OsgiPluginDeinstalledHelper(key, oldHelper.isRemotePlugin());
580                     setPluginState(PluginState.UNINSTALLED);
581                     oldHelper.onUninstall();
582                 }
583                 else
584                 {
585                     log.debug("Trying to uninstall '{}', but it is not installed (helper '{}')", key, helper);
586                 }
587                 break;
588 
589             }
590             catch (final BundleException e)
591             {
592                 rootCause = retryCount == 0 ? e : rootCause;
593                 retryCount++;
594 
595                 if (retryCount < 3)
596                 {
597                     log.debug("Possible transient fail on try {} to uninstall '{}', retrying in {} mSecs",
598                             new Object[] { retryCount, key, sleepTime} );
599                     log.debug(e.getMessage(), e);
600                     try
601                     {
602                         Thread.sleep(sleepTime);
603                     }
604                     catch (final InterruptedException ei)
605                     {
606                         throw new OsgiContainerException(
607                                 "Cannot uninstall '" + key + "', retry sleep was interrupted: " + ei.getMessage());
608                     }
609                 }
610                 else
611                 {
612                     log.error("Detected an error (BundleException) disabling the plugin '{}'.", key);
613                     log.error(rootCause.getMessage(), rootCause);
614                     throw new OsgiContainerException("Cannot uninstall '" + key + "'");
615                 }
616             }
617         }
618     }
619 
620     /**
621      * Obtain the bundle for this plugin if it is installed.
622      * <p/>
623      * This method really belongs on {@link OsgiPluginHelper}, but i don't want to change it's interface. It is intended to
624      * be called in a context where we expect this OsgiPlugin to be installed.
625      *
626      * @return the bundle for this plugin, or null if the plugin is not installed.
627      */
628     private Bundle getBundleIfInstalled()
629     {
630         // This implementation is ugly on account of this really belonging elsewhere. Since the intended usage is to be
631         // called only when this OsgiPlugin is installed, we don't expect to take the exception path.
632         try
633         {
634             return getBundle();
635         }
636         catch (final IllegalPluginStateException eips)
637         {
638             return null;
639         }
640     }
641 
642     /**
643      * Adds a module descriptor XML element for later processing, needed for dynamic module support
644      *
645      * @param key The module key
646      * @param element The module element
647      */
648     void addModuleDescriptorElement(final String key, final Element element)
649     {
650         moduleElements.put(key, element);
651     }
652 
653     /**
654      * Exposes {@link #removeModuleDescriptor(String)} for package-protected classes
655      *
656      * @param key The module descriptor key
657      */
658     void clearModuleDescriptor(String key)
659     {
660         removeModuleDescriptor(key);
661     }
662 
663     /**
664      * Gets the module elements for dynamic module descriptor handling.  Doesn't need to return a copy or anything
665      * immutable because it is only accessed by package-private helper classes
666      *
667      * @return The map of module keys to module XML elements
668      */
669     Map<String, Element> getModuleElements()
670     {
671         return moduleElements;
672     }
673 
674     /**
675      * Extracts the {@link PackageAdmin} instance from the OSGi container
676      * @param mgr The OSGi container manager
677      * @return The package admin instance, should never be null
678      */
679     private PackageAdmin extractPackageAdminFromOsgi(OsgiContainerManager mgr)
680     {
681         // Get the system bundle (always bundle 0)
682         Bundle bundle = mgr.getBundles()[0];
683 
684         // We assume the package admin will always be available
685         final ServiceReference ref = bundle.getBundleContext()
686                 .getServiceReference(PackageAdmin.class.getName());
687         return (PackageAdmin) bundle.getBundleContext()
688                 .getService(ref);
689     }
690 
691     public ContainerAccessor getContainerAccessor()
692     {
693         return helper.getContainerAccessor();
694     }
695 
696     @Override
697     public void resolve()
698     {
699         // It would be nicer to do this more globally, but i can't see a convenient place to hook into the OSGi interface
700         // layer in DefaultPluginManager or similar. We just resolve unconditionally for simplicity. In fact Felix grabs
701         // a lock internally before it checks the state to find there's no work, but a little artisnal data suggests the
702         // effect is neglible on jira statup (25 ms).
703         packageAdmin.resolveBundles(new Bundle[] { getBundle() });
704     }
705 
706     private static class OutstandingDependency
707     {
708         private final String beanName;
709         private final String filter;
710 
711         public OutstandingDependency(String beanName, String filter)
712         {
713             this.beanName = beanName;
714             this.filter = filter;
715         }
716 
717         @Override
718         public boolean equals(Object o)
719         {
720             if (this == o)
721             {
722                 return true;
723             }
724             if (o == null || getClass() != o.getClass())
725             {
726                 return false;
727             }
728 
729             OutstandingDependency that = (OutstandingDependency) o;
730 
731             if (beanName != null ? !beanName.equals(that.beanName) : that.beanName != null)
732             {
733                 return false;
734             }
735             if (!filter.equals(that.filter))
736             {
737                 return false;
738             }
739 
740             return true;
741         }
742 
743         @Override
744         public int hashCode()
745         {
746             int result = beanName != null ? beanName.hashCode() : 0;
747             result = 31 * result + filter.hashCode();
748             return result;
749         }
750 
751         public String toString()
752         {
753             return "service '" + beanName + "' with filter '" + filter + "'";
754         }
755     }
756 }