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