View Javadoc
1   package com.atlassian.plugin.osgi.container.felix;
2   
3   import com.atlassian.plugin.ReferenceMode;
4   import com.atlassian.plugin.event.PluginEventListener;
5   import com.atlassian.plugin.event.PluginEventManager;
6   import com.atlassian.plugin.event.events.PluginFrameworkShutdownEvent;
7   import com.atlassian.plugin.event.events.PluginFrameworkStartingEvent;
8   import com.atlassian.plugin.event.events.PluginFrameworkWarmRestartingEvent;
9   import com.atlassian.plugin.event.events.PluginUninstalledEvent;
10  import com.atlassian.plugin.event.events.PluginUpgradedEvent;
11  import com.atlassian.plugin.instrumentation.PluginSystemInstrumentation;
12  import com.atlassian.plugin.osgi.container.OsgiContainerException;
13  import com.atlassian.plugin.osgi.container.OsgiContainerManager;
14  import com.atlassian.plugin.osgi.container.OsgiContainerStartedEvent;
15  import com.atlassian.plugin.osgi.container.OsgiContainerStoppedEvent;
16  import com.atlassian.plugin.osgi.container.OsgiPersistentCache;
17  import com.atlassian.plugin.osgi.container.PackageScannerConfiguration;
18  import com.atlassian.plugin.osgi.hostcomponents.HostComponentProvider;
19  import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
20  import com.atlassian.plugin.osgi.hostcomponents.impl.DefaultComponentRegistrar;
21  import com.atlassian.plugin.osgi.util.OsgiHeaderUtil;
22  import com.atlassian.plugin.util.ContextClassLoaderSwitchingUtil;
23  import com.atlassian.plugin.util.PluginUtils;
24  import com.google.common.annotations.VisibleForTesting;
25  import org.apache.commons.lang3.StringUtils;
26  import org.apache.felix.framework.Felix;
27  import org.apache.felix.framework.Logger;
28  import org.apache.felix.framework.cache.BundleArchive;
29  import org.apache.felix.framework.cache.BundleCache;
30  import org.apache.felix.framework.util.FelixConstants;
31  import org.apache.felix.framework.util.StringMap;
32  import org.osgi.framework.Bundle;
33  import org.osgi.framework.BundleActivator;
34  import org.osgi.framework.BundleContext;
35  import org.osgi.framework.BundleEvent;
36  import org.osgi.framework.BundleException;
37  import org.osgi.framework.BundleListener;
38  import org.osgi.framework.Constants;
39  import org.osgi.framework.FrameworkEvent;
40  import org.osgi.framework.FrameworkListener;
41  import org.osgi.framework.ServiceReference;
42  import org.osgi.framework.ServiceRegistration;
43  import org.osgi.service.packageadmin.PackageAdmin;
44  import org.osgi.util.tracker.ServiceTracker;
45  import org.osgi.util.tracker.ServiceTrackerCustomizer;
46  import org.slf4j.LoggerFactory;
47  
48  import java.io.File;
49  import java.net.URL;
50  import java.util.ArrayList;
51  import java.util.Collection;
52  import java.util.Collections;
53  import java.util.HashSet;
54  import java.util.List;
55  import java.util.Map;
56  import java.util.Optional;
57  import java.util.concurrent.CountDownLatch;
58  import java.util.concurrent.ThreadFactory;
59  import java.util.concurrent.TimeUnit;
60  import java.util.function.Function;
61  
62  import static com.atlassian.plugin.util.FileUtils.conditionallyExtractZipFile;
63  import static com.google.common.base.Preconditions.checkNotNull;
64  
65  /**
66   * Felix implementation of the OSGi container manager
67   */
68  public class FelixOsgiContainerManager implements OsgiContainerManager {
69      public static final int REFRESH_TIMEOUT = 10;
70  
71      private static final org.slf4j.Logger log = LoggerFactory.getLogger(FelixOsgiContainerManager.class);
72      public static final String ATLASSIAN_BOOTDELEGATION = "atlassian.org.osgi.framework.bootdelegation";
73      public static final String ATLASSIAN_BOOTDELEGATION_EXTRA = "atlassian.org.osgi.framework.bootdelegation.extra";
74      public static final String ATLASSIAN_DISABLE_REFERENCE_PROTOCOL = "atlassian.felix.disable.reference.protocol";
75  
76      private final OsgiPersistentCache persistentCache;
77      private Function<DefaultComponentRegistrar, BundleRegistration> bundleRegistrationFactory;
78      private final PackageScannerConfiguration packageScannerConfig;
79      private final HostComponentProvider hostComponentProvider;
80      private final List<ServiceTracker> trackers;
81      private final ExportsBuilder exportsBuilder;
82  
83      private final ThreadFactory threadFactory = runnable -> {
84          final Thread thread = new Thread(runnable, "Felix:Startup");
85          thread.setDaemon(true);
86          return thread;
87      };
88      private BundleRegistration registration = null;
89      private Felix felix = null;
90      private boolean felixRunning = false;
91      private boolean disableMultipleBundleVersions = true;
92      private Logger felixLogger;
93      private final PluginEventManager pluginEventManager;
94  
95      /**
96       * Constructs the container manager.
97       *
98       * @param frameworkBundlesZip  The location of the zip file containing framework bundles
99       * @param persistentCache      The persistent cache to use for the framework and framework bundles
100      * @param packageScannerConfig The configuration for package scanning
101      * @param provider             The host component provider. May be null.
102      * @param eventManager         The plugin event manager to register for init and shutdown events
103      * @throws OsgiContainerException If the host version isn't supplied and the cache directory cannot be cleaned.
104      * @since 2.2.0
105      */
106     public FelixOsgiContainerManager(
107             final URL frameworkBundlesZip,
108             final OsgiPersistentCache persistentCache,
109             final PackageScannerConfiguration packageScannerConfig,
110             final HostComponentProvider provider,
111             final PluginEventManager eventManager)
112             throws OsgiContainerException {
113         this(registrar -> new BundleRegistration(frameworkBundlesZip, persistentCache.getFrameworkBundleCache(), registrar),
114                 persistentCache, packageScannerConfig, provider, eventManager);
115     }
116 
117     /**
118      * Constructs the container manager.
119      * <p>
120      * This constructor is used when frameworkBundlesDirectory has already been populated with the framework bundles. It
121      * ignores {@link OsgiPersistentCache#getFrameworkBundleCache}.
122      *
123      * @param frameworkBundlesDirectory The directory containing framework bundles
124      * @param persistentCache           The persistent cache to use for the framework
125      * @param packageScannerConfig      The configuration for package scanning
126      * @param provider                  The host component provider. May be null.
127      * @param eventManager              The plugin event manager to register for init and shutdown events
128      * @throws OsgiContainerException If the host version isn't supplied and the cache directory cannot be cleaned.
129      * @since 3.0.21
130      */
131     public FelixOsgiContainerManager(
132             final File frameworkBundlesDirectory,
133             final OsgiPersistentCache persistentCache,
134             final PackageScannerConfiguration packageScannerConfig,
135             final HostComponentProvider provider,
136             final PluginEventManager eventManager)
137             throws OsgiContainerException {
138         this(registrar -> new BundleRegistration(frameworkBundlesDirectory, registrar), persistentCache,
139                 packageScannerConfig, provider, eventManager);
140     }
141 
142     /**
143      * Constructs the container manager.
144      * <p>
145      * This constructor is private because i would like to move to using a builder for classes like this with many
146      * constructors and lots of defaults. In any case, it uses internal classes (BundleRegistration) to abstract constructor
147      * differences, so it's not useful to clients in general.
148      *
149      * @param bundleRegistrationFactory The factory for the BundleRegistration helper
150      * @param persistentCache           The persistent cache to use for the framework
151      * @param packageScannerConfig      The configuration for package scanning
152      * @param provider                  The host component provider. May be null.
153      * @param eventManager              The plugin event manager to register for init and shutdown events
154      * @throws OsgiContainerException If the host version isn't supplied and the cache directory cannot be cleaned.
155      * @since 3.0.21
156      */
157     private FelixOsgiContainerManager(
158             final Function<DefaultComponentRegistrar, BundleRegistration> bundleRegistrationFactory,
159             final OsgiPersistentCache persistentCache,
160             final PackageScannerConfiguration packageScannerConfig,
161             final HostComponentProvider provider,
162             final PluginEventManager eventManager)
163             throws OsgiContainerException {
164         checkNotNull(bundleRegistrationFactory, "The bundle registration factory must not be null");
165         checkNotNull(persistentCache, "The framework bundles directory must not be null");
166         checkNotNull(packageScannerConfig, "The package scanner configuration must not be null");
167         checkNotNull(eventManager, "The plugin event manager must not be null");
168 
169         this.bundleRegistrationFactory = bundleRegistrationFactory;
170         this.packageScannerConfig = packageScannerConfig;
171         this.persistentCache = persistentCache;
172         hostComponentProvider = provider;
173         trackers = Collections.synchronizedList(new ArrayList<>());
174         this.pluginEventManager = eventManager;
175         eventManager.register(this);
176         felixLogger = new FelixLoggerBridge(log);
177         exportsBuilder = new ExportsBuilder();
178     }
179 
180     public void setFelixLogger(final Logger logger) {
181         felixLogger = logger;
182     }
183 
184     public void setDisableMultipleBundleVersions(final boolean val) {
185         disableMultipleBundleVersions = val;
186     }
187 
188     /**
189      * Clears export cache.
190      *
191      * @since 2.9.0
192      */
193     public void clearExportCache() {
194         exportsBuilder.clearExportCache();
195     }
196 
197     @SuppressWarnings({"UnusedDeclaration"})
198     @PluginEventListener
199     public void onStart(final PluginFrameworkStartingEvent event) {
200         start();
201     }
202 
203     @SuppressWarnings({"UnusedDeclaration"})
204     @PluginEventListener
205     public void onShutdown(final PluginFrameworkShutdownEvent event) {
206         stop();
207     }
208 
209     @SuppressWarnings({"UnusedDeclaration"})
210     @PluginEventListener
211     public void onPluginUpgrade(final PluginUpgradedEvent event) {
212         registration.refreshPackages();
213     }
214 
215     @SuppressWarnings({"UnusedDeclaration"})
216     @PluginEventListener
217     public void onPluginUninstallation(final PluginUninstalledEvent event) {
218         registration.refreshPackages();
219     }
220 
221     @SuppressWarnings({"UnusedDeclaration"})
222     @PluginEventListener
223     public void onPluginFrameworkWarmRestarting(final PluginFrameworkWarmRestartingEvent event) {
224         registration.loadHostComponents(collectHostComponents(hostComponentProvider));
225     }
226 
227     public void start() throws OsgiContainerException {
228         if (isRunning()) {
229             return;
230         }
231 
232         final DefaultComponentRegistrar registrar = collectHostComponents(hostComponentProvider);
233         // Create a case-insensitive configuration property map.
234         final StringMap configMap = new StringMap();
235 
236         // Add the bundle provided service interface package and the core OSGi
237         // packages to be exported from the class path via the system bundle.
238         configMap.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, exportsBuilder.getExports(registrar.getRegistry(), packageScannerConfig));
239 
240         // Explicitly specify the directory to use for caching bundles.
241         configMap.put(BundleCache.CACHE_ROOTDIR_PROP, persistentCache.getOsgiBundleCache().getAbsolutePath());
242 
243         configMap.put(FelixConstants.LOG_LEVEL_PROP, String.valueOf(felixLogger.getLogLevel()));
244         configMap.put(FelixConstants.LOG_LOGGER_PROP, felixLogger);
245         String bootDelegation = System.getProperty(ATLASSIAN_BOOTDELEGATION);
246         if ((bootDelegation == null) || (bootDelegation.trim().length() == 0)) {
247             // These exist to work around JAXP problems. Specifically, bundles that use static factories to create JAXP
248             // instances will execute FactoryFinder with the CCL set to the bundle. These delegations ensure the appropriate
249             // implementation is found and loaded.
250             bootDelegation = "weblogic,weblogic.*," +
251                     "META-INF.services," +
252                     "jdk.*," +
253                     "com.yourkit,com.yourkit.*," +
254                     "com.chronon,com.chronon.*," +
255                     "org.jboss.byteman,org.jboss.byteman.*," +
256                     "com.jprofiler,com.jprofiler.*," +
257                     "org.apache.xerces,org.apache.xerces.*," +
258                     "org.apache.xalan,org.apache.xalan.*," +
259                     "org.apache.xml.serializer," +
260                     "sun.*," +
261                     "com.sun.xml.bind.v2," +
262                     "com.sun.xml.internal.bind.v2," +
263                     "com.icl.saxon," +
264                     "com_cenqua_clover," +
265                     "com.cenqua.clover,com.cenqua.clover.*," +
266                     "com.atlassian.clover,com.atlassian.clover.*";
267         }
268         final String extraBootDelegation = System.getProperty(ATLASSIAN_BOOTDELEGATION_EXTRA, "").trim();
269         if (0 != extraBootDelegation.length()) {
270             bootDelegation = bootDelegation + "," + extraBootDelegation;
271         }
272 
273         configMap.put(FelixConstants.FRAMEWORK_BOOTDELEGATION, bootDelegation);
274         configMap.put(FelixConstants.IMPLICIT_BOOT_DELEGATION_PROP, "false");
275 
276         configMap.put(FelixConstants.FRAMEWORK_BUNDLE_PARENT, FelixConstants.FRAMEWORK_BUNDLE_PARENT_FRAMEWORK);
277         if (log.isDebugEnabled()) {
278             log.debug("Felix configuration: " + configMap);
279         }
280 
281         validateConfiguration(configMap);
282 
283         try {
284             // Create host activator;
285             registration = bundleRegistrationFactory.apply(registrar);
286             final List<BundleActivator> list = new ArrayList<>();
287             list.add(registration);
288             configMap.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, list);
289 
290             // Now create an instance of the framework with
291             // our configuration properties and activator.
292             felix = new Felix(configMap);
293 
294             // Now start Felix instance. Starting in a different thread to explicitly set daemon status
295             final Runnable start = () -> {
296                 try {
297                     Thread.currentThread().setContextClassLoader(null);
298                     felix.start();
299                     felixRunning = true;
300                 } catch (final BundleException e) {
301                     throw new OsgiContainerException("Unable to start felix", e);
302                 }
303             };
304             final Thread t = threadFactory.newThread(start);
305             t.start();
306 
307             // Give it 10 seconds
308             t.join(10 * 60 * 1000);
309 
310         } catch (final Exception ex) {
311             throw new OsgiContainerException("Unable to start OSGi container", ex);
312         }
313         pluginEventManager.broadcast(new OsgiContainerStartedEvent(this));
314     }
315 
316     /**
317      * @param configMap The Felix configuration
318      * @throws OsgiContainerException If any validation fails
319      */
320     private void validateConfiguration(final StringMap configMap) throws OsgiContainerException {
321         final String systemExports = (String) configMap.get(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA);
322         // Change in JVM should trigger the cache cleardown, regardless of export changes.
323         final String cacheKeySource = StringUtils.join(new Object[]{this.getRuntimeEnvironment(), systemExports}, ',');
324 
325         validateCaches(cacheKeySource);
326 
327         detectIncorrectOsgiVersion();
328         detectXercesOverride(systemExports);
329     }
330 
331     /**
332      * Detect when xerces has no version, most likely due to an installation of Tomcat where an old version of xerces is
333      * installed into common/lib/endorsed in order to support Java 1.4.
334      *
335      * @param systemExports The system exports
336      * @throws OsgiContainerException If xerces has no version
337      */
338     void detectXercesOverride(final String systemExports) throws OsgiContainerException {
339         int pos = systemExports.indexOf("org.apache.xerces.util");
340         if (pos > -1) {
341             if (pos == 0 || systemExports.charAt(pos - 1) == ',') {
342                 pos += "org.apache.xerces.util".length();
343 
344                 // only fail if no xerces found and xerces has no version
345                 if (pos >= systemExports.length() || ';' != systemExports.charAt(pos)) {
346                     throw new OsgiContainerException(
347                             "Detected an incompatible version of Apache Xerces on the classpath. If using Tomcat, you may have " +
348                                     "an old version of Xerces in $TOMCAT_HOME/common/lib/endorsed that will need to be removed."
349                     );
350                 }
351             }
352         }
353     }
354 
355     /**
356      * Validate caches based on the list of packages and the runtime environment exported from the application. If the
357      * settings have changed, the cache directories should be cleared.
358      *
359      * @param cacheKeySource string of information that generates the cache key.
360      */
361     private void validateCaches(final String cacheKeySource) {
362         log.info("Using Felix bundle cacheKey source: {}", cacheKeySource);
363         persistentCache.validate(cacheKeySource);
364         log.debug("Using Felix bundle cache directory: {}", persistentCache.getOsgiBundleCache().getAbsolutePath());
365     }
366 
367     /**
368      * Detects incorrect configuration of WebSphere 6.1 that leaks OSGi 4.0 jars into the application
369      */
370     private void detectIncorrectOsgiVersion() {
371         try {
372             Bundle.class.getMethod("getBundleContext");
373         } catch (final NoSuchMethodException e) {
374             throw new OsgiContainerException("Detected older version (4.0 or earlier) of OSGi. If using WebSphere 6.1, "
375                     + "please enable application-first (parent-last) classloading and the 'Single classloader for "
376                     + "application' WAR classloader policy.");
377         }
378     }
379 
380     public void stop() throws OsgiContainerException {
381         if (felixRunning) {
382             for (final ServiceTracker tracker : new HashSet<>(trackers)) {
383                 tracker.close();
384             }
385             final FrameworkListener listener = event -> {
386                 if (event.getType() == FrameworkEvent.WAIT_TIMEDOUT) {
387                     log.error("Timeout waiting for OSGi to shutdown");
388                     threadDump();
389                 } else if (event.getType() == FrameworkEvent.STOPPED) {
390                     log.info("OSGi shutdown successful");
391                 }
392             };
393             try {
394                 felix.getBundleContext().addFrameworkListener(listener);
395                 felix.stop();
396                 felix.waitForStop(TimeUnit.SECONDS.toMillis(60));
397             } catch (final InterruptedException e) {
398                 log.warn("Interrupting Felix shutdown", e);
399             } catch (final BundleException ex) {
400                 log.error("An error occurred while stopping the Felix OSGi Container. ", ex);
401             }
402         }
403 
404         felixRunning = false;
405         felix = null;
406         pluginEventManager.broadcast(new OsgiContainerStoppedEvent(this));
407     }
408 
409     private void threadDump() {
410         final StringBuilder sb = new StringBuilder();
411         final String nl = System.getProperty("line.separator");
412         for (final Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
413             final Thread key = entry.getKey();
414             final StackTraceElement[] trace = entry.getValue();
415             sb.append(key).append(nl);
416             for (final StackTraceElement aTrace : trace) {
417                 sb.append(" ").append(aTrace).append(nl);
418             }
419         }
420         log.debug("Thread dump: " + nl + sb.toString());
421     }
422 
423     public Bundle[] getBundles() {
424         if (isRunning()) {
425             return registration.getBundles();
426         } else {
427             throw new IllegalStateException("Cannot retrieve the bundles if the Felix container isn't running. Check "
428                     + "earlier in the logs for the possible cause as to why Felix didn't start correctly.");
429         }
430     }
431 
432     public ServiceReference[] getRegisteredServices() {
433         return felix.getRegisteredServices();
434     }
435 
436     public ServiceTracker getServiceTracker(final String interfaceClassName) {
437         return getServiceTracker(interfaceClassName, null);
438     }
439 
440     @Override
441     public ServiceTracker getServiceTracker(
442             final String interfaceClassName,
443             final ServiceTrackerCustomizer serviceTrackerCustomizer) {
444         if (!isRunning()) {
445             throw new IllegalStateException("Unable to create a tracker when osgi is not running");
446         }
447 
448         final ServiceTracker tracker = registration.getServiceTracker(interfaceClassName, trackers, serviceTrackerCustomizer);
449         tracker.open();
450         trackers.add(tracker);
451         return tracker;
452     }
453 
454     @Override
455     public void addBundleListener(BundleListener listener) {
456         felix.getBundleContext().addBundleListener(listener);
457     }
458 
459     @Override
460     public void removeBundleListener(BundleListener listener) {
461         Felix felix = this.felix;
462         if (felix != null) {
463             BundleContext context = felix.getBundleContext();
464             if (context != null) {
465                 context.removeBundleListener(listener);
466             }
467         }
468     }
469 
470     @Override
471     public Bundle installBundle(final File file, final ReferenceMode referenceMode) throws OsgiContainerException {
472         try {
473             return registration.install(file, disableMultipleBundleVersions, referenceMode.allowsReference());
474         } catch (final BundleException e) {
475             throw new OsgiContainerException("Unable to install bundle", e);
476         }
477     }
478 
479     DefaultComponentRegistrar collectHostComponents(final HostComponentProvider provider) {
480         final DefaultComponentRegistrar registrar = new DefaultComponentRegistrar();
481         if (provider != null) {
482             provider.provide(registrar);
483         }
484         return registrar;
485     }
486 
487     public boolean isRunning() {
488         return felixRunning;
489     }
490 
491     public List<HostComponentRegistration> getHostComponentRegistrations() {
492         return registration.getHostComponentRegistrations();
493     }
494 
495     /**
496      * Get a string containing the significant runtime environment details that may require a rebuild of the OSGi cache.
497      *
498      * @return a string containing significant runtime details.
499      */
500     @VisibleForTesting
501     String getRuntimeEnvironment() {
502         //Two things currently:
503         // - JDK version
504         // - Startup timeout which may be cached in the transformed plugins.
505         return String.format("java.version=%s,plugin.enable.timeout=%d", System.getProperty("java.version"),
506                 PluginUtils.getDefaultEnablingWaitPeriod());
507     }
508 
509     /**
510      * Manages framework-level framework bundles and host components registration, and individual plugin bundle installation
511      * and removal.
512      */
513     static class BundleRegistration implements BundleActivator, BundleListener, FrameworkListener {
514 
515         private final URL frameworkBundlesUrl;
516         private final File frameworkBundlesDir;
517         private DefaultComponentRegistrar registrar;
518         private ClassLoader initializedClassLoader;
519 
520         private BundleContext bundleContext;
521         private PackageAdmin packageAdmin;
522         private List<ServiceRegistration> hostServicesReferences;
523         private List<HostComponentRegistration> hostComponentRegistrations;
524         private Optional<ServiceRegistration> instrumentationServiceReference = Optional.empty();
525 
526         public BundleRegistration(final File frameworkBundlesDir, final DefaultComponentRegistrar registrar) {
527             this(null, frameworkBundlesDir, registrar);
528         }
529 
530         public BundleRegistration(final URL frameworkBundlesUrl, final File frameworkBundlesDir, final DefaultComponentRegistrar registrar) {
531             this.frameworkBundlesUrl = frameworkBundlesUrl;
532             this.frameworkBundlesDir = frameworkBundlesDir;
533             this.registrar = registrar;
534             this.initializedClassLoader = Thread.currentThread().getContextClassLoader();
535         }
536 
537         public void start(final BundleContext context) throws Exception {
538             bundleContext = context;
539             final ServiceReference ref = context.getServiceReference(PackageAdmin.class.getName());
540             packageAdmin = (PackageAdmin) context.getService(ref);
541 
542             context.addBundleListener(this);
543             context.addFrameworkListener(this);
544 
545             loadHostComponents(registrar);
546             if (null != frameworkBundlesUrl) {
547                 conditionallyExtractZipFile(frameworkBundlesUrl, frameworkBundlesDir);
548             }
549             installFrameworkBundles();
550 
551             // unregister the optional instrumentation service
552             instrumentationServiceReference.ifPresent(ServiceRegistration::unregister);
553             instrumentationServiceReference = Optional.empty();
554 
555             // register the instrumentation service if it's present, keeping aware that InstrumentRegistry might not be in the class loader
556             final Optional instrumentRegistry = PluginSystemInstrumentation.instance().getInstrumentRegistry();
557             if (instrumentRegistry.isPresent()) {
558                 instrumentationServiceReference = Optional.of(context.registerService(PluginSystemInstrumentation.INSTRUMENT_REGISTRY_CLASS, instrumentRegistry.get(), null));
559             } else {
560                 instrumentationServiceReference = Optional.empty();
561             }
562         }
563 
564         public void stop(final BundleContext ctx) {
565             ctx.removeBundleListener(this);
566             ctx.removeFrameworkListener(this);
567             if (hostServicesReferences != null) {
568                 for (final ServiceRegistration ref : hostServicesReferences) {
569                     ref.unregister();
570                 }
571             }
572 
573             // unregister the optional instrumentation service
574             instrumentationServiceReference.ifPresent(ServiceRegistration::unregister);
575             instrumentationServiceReference = Optional.empty();
576 
577             bundleContext = null;
578             packageAdmin = null;
579             hostServicesReferences = null;
580             hostComponentRegistrations = null;
581             registrar = null;
582             initializedClassLoader = null;
583         }
584 
585         public void bundleChanged(final BundleEvent evt) {
586             switch (evt.getType()) {
587                 case BundleEvent.INSTALLED:
588                     log.info("Installed bundle " + evt.getBundle().getSymbolicName() + " (" + evt.getBundle().getBundleId() + ")");
589                     break;
590                 case BundleEvent.RESOLVED:
591                     log.info("Resolved bundle " + evt.getBundle().getSymbolicName() + " (" + evt.getBundle().getBundleId() + ")");
592                     break;
593                 case BundleEvent.UNRESOLVED:
594                     log.info("Unresolved bundle " + evt.getBundle().getSymbolicName() + " (" + evt.getBundle().getBundleId() + ")");
595                     break;
596                 case BundleEvent.STARTED:
597                     log.info("Started bundle " + evt.getBundle().getSymbolicName() + " (" + evt.getBundle().getBundleId() + ")");
598                     break;
599                 case BundleEvent.STOPPED:
600                     log.info("Stopped bundle " + evt.getBundle().getSymbolicName() + " (" + evt.getBundle().getBundleId() + ")");
601                     break;
602                 case BundleEvent.UNINSTALLED:
603                     log.info("Uninstalled bundle " + evt.getBundle().getSymbolicName() + " (" + evt.getBundle().getBundleId() + ")");
604                     break;
605             }
606         }
607 
608         public Bundle install(final File path, final boolean uninstallOtherVersions)
609                 throws BundleException {
610             return install(path, uninstallOtherVersions, false);
611         }
612 
613         public Bundle install(final File path, final boolean uninstallOtherVersions, final boolean allowReference)
614                 throws BundleException {
615             boolean bundleUninstalled = false;
616             if (uninstallOtherVersions) {
617                 final String pluginKey = OsgiHeaderUtil.getPluginKey(path);
618                 if (null == pluginKey) {
619                     // If the jar we're installing doesn't have a manifest, felix will choke on it in short order,
620                     // so we just short circuit that to avoid NPE's if we have a malformed jar.
621                     throw new BundleException("No plugin key in (possibly malformed) bundle jar '" + path + "'");
622                 }
623 
624                 for (final Bundle oldBundle : bundleContext.getBundles()) {
625                     if (pluginKey.equals(OsgiHeaderUtil.getPluginKey(oldBundle))) {
626                         log.info("Uninstalling existing version " + oldBundle.getHeaders().get(Constants.BUNDLE_VERSION));
627                         oldBundle.uninstall();
628                         bundleUninstalled = true;
629                     }
630                 }
631             }
632             String location = path.toURI().toString();
633             if (allowReference &&
634                     (!Boolean.getBoolean(ATLASSIAN_DISABLE_REFERENCE_PROTOCOL)) &&
635                     location.startsWith(BundleArchive.FILE_PROTOCOL)) {
636                 // This will avoid copying of the bundle jar artifact to the felix-cache
637                 // location, saving I/O and disk space.
638                 location = BundleArchive.REFERENCE_PROTOCOL + location;
639             }
640             final Bundle bundle = bundleContext.installBundle(location);
641             if (bundleUninstalled) {
642                 refreshPackages();
643             }
644             return bundle;
645         }
646 
647         public Bundle[] getBundles() {
648             return bundleContext.getBundles();
649         }
650 
651         public ServiceTracker getServiceTracker(final String clazz, final Collection<ServiceTracker> trackedTrackers) {
652             return getServiceTracker(clazz, trackedTrackers, null);
653         }
654 
655         public ServiceTracker getServiceTracker(final String clazz,
656                                                 final Collection<ServiceTracker> trackedTrackers,
657                                                 final ServiceTrackerCustomizer customizer) {
658             return new ServiceTracker(bundleContext, clazz, customizer) {
659                 @Override
660                 public void close() {
661                     super.close();
662                     trackedTrackers.remove(this);
663                 }
664             };
665         }
666 
667         public List<HostComponentRegistration> getHostComponentRegistrations() {
668             return hostComponentRegistrations;
669         }
670 
671         void loadHostComponents(final DefaultComponentRegistrar registrar) {
672             // Unregister any existing host components
673             if (hostServicesReferences != null) {
674                 for (final ServiceRegistration reg : hostServicesReferences) {
675                     reg.unregister();
676                 }
677             }
678 
679             ContextClassLoaderSwitchingUtil.runInContext(initializedClassLoader, () -> {
680                 hostServicesReferences = registrar.writeRegistry(bundleContext);
681                 hostComponentRegistrations = registrar.getRegistry();
682             });
683         }
684 
685         private void installFrameworkBundles() throws BundleException {
686             final File[] bundleFiles = frameworkBundlesDir == null
687                     ? null : frameworkBundlesDir.listFiles((file, s) -> s.endsWith(".jar"));
688             if (bundleFiles == null) {
689                 throw new BundleException("Directory with framework bundle jars could not be read: " + frameworkBundlesDir);
690             }
691 
692             final List<Bundle> bundles = new ArrayList<>();
693             for (final File bundleFile : bundleFiles) {
694                 bundles.add(install(bundleFile, false, false));
695             }
696 
697             packageAdmin.resolveBundles(null);
698 
699             for (final Bundle bundle : bundles) {
700                 if (bundle.getHeaders().get(Constants.FRAGMENT_HOST) == null) {
701                     bundle.start();
702                 }
703             }
704         }
705 
706         public void refreshPackages() {
707             final CountDownLatch latch = new CountDownLatch(1);
708             final FrameworkListener refreshListener = event -> {
709                 if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) {
710                     log.info("Packages refreshed");
711                     latch.countDown();
712                 }
713             };
714 
715             bundleContext.addFrameworkListener(refreshListener);
716             try {
717                 packageAdmin.refreshPackages(null);
718                 boolean refreshed = false;
719                 try {
720                     refreshed = latch.await(REFRESH_TIMEOUT, TimeUnit.SECONDS);
721                 } catch (final InterruptedException e) {
722                     // ignore
723                 }
724                 if (!refreshed) {
725                     log.warn("Timeout exceeded waiting for package refresh");
726                 }
727             } finally {
728                 bundleContext.removeFrameworkListener(refreshListener);
729             }
730         }
731 
732         @SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
733         public void frameworkEvent(final FrameworkEvent event) {
734             String bundleBits = "";
735             if (event.getBundle() != null) {
736                 bundleBits = " in bundle " + event.getBundle().getSymbolicName();
737             }
738             switch (event.getType()) {
739                 case FrameworkEvent.ERROR:
740                     log.error("Framework error" + bundleBits, event.getThrowable());
741                     break;
742                 case FrameworkEvent.WARNING:
743                     log.warn("Framework warning" + bundleBits, event.getThrowable());
744                     break;
745                 case FrameworkEvent.INFO:
746                     log.info("Framework info" + bundleBits, event.getThrowable());
747                     break;
748             }
749         }
750     }
751 
752 }