View Javadoc
1   package com.atlassian.plugin.osgi.container.felix;
2   
3   import com.atlassian.plugin.ReferenceMode;
4   import com.atlassian.plugin.event.impl.DefaultPluginEventManager;
5   import com.atlassian.plugin.osgi.container.OsgiContainerException;
6   import com.atlassian.plugin.osgi.container.OsgiPersistentCache;
7   import com.atlassian.plugin.osgi.container.impl.DefaultOsgiPersistentCache;
8   import com.atlassian.plugin.osgi.container.impl.DefaultPackageScannerConfiguration;
9   import com.atlassian.plugin.test.PluginJarBuilder;
10  import com.atlassian.plugin.test.PluginTestUtils;
11  import com.google.common.base.Charsets;
12  import com.google.common.collect.ImmutableList;
13  import com.google.common.collect.ImmutableMap;
14  import com.google.common.hash.Hashing;
15  import org.apache.commons.io.FileUtils;
16  import org.apache.felix.framework.Logger;
17  import org.apache.felix.framework.cache.BundleArchive;
18  import org.hamcrest.FeatureMatcher;
19  import org.hamcrest.Matcher;
20  import org.junit.After;
21  import org.junit.Before;
22  import org.junit.Rule;
23  import org.junit.Test;
24  import org.junit.contrib.java.lang.system.RestoreSystemProperties;
25  import org.osgi.framework.Bundle;
26  import org.osgi.framework.BundleException;
27  import org.osgi.framework.Constants;
28  import org.osgi.framework.ServiceReference;
29  import org.osgi.service.packageadmin.PackageAdmin;
30  import org.osgi.util.tracker.ServiceTracker;
31  import org.osgi.util.tracker.ServiceTrackerCustomizer;
32  
33  import java.io.File;
34  import java.io.FilenameFilter;
35  import java.io.IOException;
36  import java.net.URISyntaxException;
37  import java.net.URL;
38  import java.util.ArrayList;
39  import java.util.Collection;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.concurrent.CountDownLatch;
43  import java.util.concurrent.TimeUnit;
44  
45  import static com.atlassian.plugin.ReferenceMode.FORBID_REFERENCE;
46  import static com.atlassian.plugin.ReferenceMode.PERMIT_REFERENCE;
47  import static com.atlassian.plugin.osgi.container.felix.FelixOsgiContainerManager.ATLASSIAN_BOOTDELEGATION;
48  import static com.atlassian.plugin.osgi.container.felix.FelixOsgiContainerManager.ATLASSIAN_BOOTDELEGATION_EXTRA;
49  import static com.atlassian.plugin.osgi.container.felix.FelixOsgiContainerManager.ATLASSIAN_DISABLE_REFERENCE_PROTOCOL;
50  import static com.atlassian.plugin.test.Matchers.fileNamed;
51  import static com.atlassian.plugin.util.PluginUtils.ATLASSIAN_PLUGINS_ENABLE_WAIT;
52  import static org.hamcrest.Matchers.allOf;
53  import static org.hamcrest.Matchers.arrayContaining;
54  import static org.hamcrest.Matchers.arrayWithSize;
55  import static org.hamcrest.Matchers.containsString;
56  import static org.hamcrest.Matchers.empty;
57  import static org.hamcrest.Matchers.emptyArray;
58  import static org.hamcrest.Matchers.equalTo;
59  import static org.hamcrest.Matchers.hasItemInArray;
60  import static org.hamcrest.Matchers.hasSize;
61  import static org.hamcrest.Matchers.is;
62  import static org.hamcrest.Matchers.not;
63  import static org.hamcrest.Matchers.notNullValue;
64  import static org.hamcrest.Matchers.nullValue;
65  import static org.hamcrest.Matchers.startsWith;
66  import static org.junit.Assert.assertThat;
67  import static org.junit.Assert.fail;
68  import static org.osgi.framework.Constants.BUNDLE_NAME;
69  import static org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME;
70  
71  public class TestFelixOsgiContainerManager {
72      @Rule
73      public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
74  
75      private File tmpdir;
76      private OsgiPersistentCache osgiPersistentCache;
77      private FelixOsgiContainerManager felix;
78      private URL frameworkBundlesUrl = getClass().getResource("/nothing.zip");
79  
80      @Before
81      public void setUp() throws Exception {
82          tmpdir = PluginTestUtils.createTempDirectory(TestFelixOsgiContainerManager.class);
83          osgiPersistentCache = new DefaultOsgiPersistentCache(tmpdir);
84          felix = new FelixOsgiContainerManager(
85                  frameworkBundlesUrl,
86                  osgiPersistentCache,
87                  new DefaultPackageScannerConfiguration(),
88                  null,
89                  new DefaultPluginEventManager());
90      }
91  
92      @After
93      public void tearDown() {
94          if (felix != null && felix.isRunning()) {
95              for (final Bundle bundle : felix.getBundles()) {
96                  try {
97                      bundle.uninstall();
98                  } catch (final BundleException ignored) {
99                  }
100             }
101         }
102         if (felix != null) {
103             felix.stop();
104             felix.clearExportCache(); // prevent export cache from being reused across tests
105         }
106         felix = null;
107         osgiPersistentCache = null;
108         tmpdir = null;
109     }
110 
111     @Test
112     public void testDetectXercesOverride() {
113         felix.detectXercesOverride("foo.bar,baz.jim");
114         felix.detectXercesOverride("foo.bar,org.apache.xerces.util;version=\"1.0\",baz.jim");
115         felix.detectXercesOverride("foo.bar,org.apache.xerces.util;version=\"1.0\"");
116         felix.detectXercesOverride("foo.bar,repackaged.org.apache.xerces.util,bar.baz");
117 
118 
119         try {
120             felix.detectXercesOverride("foo.bar,org.apache.xerces.util");
121             fail("Should fail validation");
122         } catch (final OsgiContainerException ex) {
123             // should fail
124         }
125 
126         try {
127             felix.detectXercesOverride("org.apache.xerces.util");
128             fail("Should fail validation");
129         } catch (final OsgiContainerException ex) {
130             // should fail
131         }
132 
133         try {
134             felix.detectXercesOverride("org.apache.xerces.util,bar.baz");
135             fail("Should fail validation");
136         } catch (final OsgiContainerException ex) {
137             // should fail
138         }
139 
140     }
141 
142     @Test
143     public void testStartStop() {
144         final FilenameFilter filter = (file, s) -> s.startsWith("felix");
145         final int filesNamedFelix = tmpdir.listFiles(filter).length;
146         felix.start();
147         assertThat(felix.isRunning(), is(true));
148         assertThat(felix.getBundles(), arrayWithSize(1));
149         felix.stop();
150         assertThat(tmpdir.listFiles(filter), arrayWithSize(filesNamedFelix));
151     }
152 
153     @Test
154     public void testInstallBundle() throws URISyntaxException {
155         felix.start();
156         assertThat(felix.getBundles(), arrayWithSize(1));
157         final File jar = new File(getClass().getResource("/myapp-1.0.jar").toURI());
158         felix.installBundle(jar, FORBID_REFERENCE);
159         assertThat(felix.getBundles(), arrayWithSize(2));
160         assertThat(felix.getBundles()[1], not(bundleThatIsReference()));
161         // There should a single jar in the felix cache now
162         final String[] justJar = {"jar"};
163         final Collection<File> cachedJars = FileUtils.listFiles(osgiPersistentCache.getOsgiBundleCache(), justJar, true);
164         assertThat(cachedJars, hasSize(1));
165     }
166 
167     @Test
168     public void testInstallBundleAllowReference() throws URISyntaxException {
169         felix.start();
170         assertThat(felix.getBundles(), arrayWithSize(1));
171         final File jar = new File(getClass().getResource("/myapp-1.0.jar").toURI());
172         felix.installBundle(jar, PERMIT_REFERENCE);
173         assertThat(felix.getBundles(), arrayWithSize(2));
174         assertThat(felix.getBundles()[1], bundleThatIsReference());
175         // There should be no jars in the felix cache
176         final String[] justJar = {"jar"};
177         final Collection<File> cachedJars = FileUtils.listFiles(osgiPersistentCache.getOsgiBundleCache(), justJar, true);
178         assertThat(cachedJars, empty());
179     }
180 
181     @Test
182     public void testInstallFileBundleWithReferenceProtocolDisabled() throws URISyntaxException {
183         felix.start();
184         assertThat(felix.getBundles(), arrayWithSize(1));
185         System.setProperty(ATLASSIAN_DISABLE_REFERENCE_PROTOCOL, "true");
186         final File jar = new File(getClass().getResource("/myapp-1.0.jar").toURI());
187         felix.installBundle(jar, ReferenceMode.PERMIT_REFERENCE);
188         assertThat(felix.getBundles(), arrayWithSize(2));
189         assertThat(felix.getBundles()[1], not(bundleThatIsReference()));
190     }
191 
192     @Test
193     public void checkDefaultBootDelegation() throws Exception {
194         felix.start();
195         final Bundle clientBundle = installBootDelegationBundles();
196         try {
197             // junit is not boot delegated by default
198             clientBundle.loadClass("my.client.ClientClass").newInstance();
199             fail("Expected exception: NoClassDefFoundError");
200         } catch (final NoClassDefFoundError expected) {
201             assertThat(expected.getMessage(), containsString("junit/framework/TestCase"));
202         }
203         // but com.sun.xml.internal.bind.v2 is
204         clientBundle.loadClass("my.client.OtherClientClass").newInstance();
205         felix.stop();
206     }
207 
208     @Test
209     public void checkReplacingBootDelegation() throws Exception {
210         // This system property replaces the boot delegation
211         System.setProperty(ATLASSIAN_BOOTDELEGATION, "junit.framework,junit.framework.*");
212         felix.start();
213         // The replacement exposed the JUnit TestCase class from the parent classloader to the bundle ...
214         final Bundle clientBundle = installBootDelegationBundles();
215         clientBundle.loadClass("my.client.ClientClass").newInstance();
216         // ... but does not include the previously default com.sun.xml.bind.v2...
217         try {
218             clientBundle.loadClass("my.client.OtherClientClass").newInstance();
219             fail("Expected exception: NoClassDefFoundError");
220         } catch (final NoClassDefFoundError expected) {
221             assertThat(expected.getMessage(), containsString("com/sun/xml/bind/v2/ContextFactory"));
222         }
223         felix.stop();
224     }
225 
226     @Test
227     public void checkExtraBootDelegation() throws Exception {
228         // This system property augments the boot delegation
229         System.setProperty(ATLASSIAN_BOOTDELEGATION_EXTRA, "junit.framework,junit.framework.*");
230         felix.start();
231         // We've now exposed the JUnit TestCase class from the parent classloader to the bundle ...
232         final Bundle clientBundle = installBootDelegationBundles();
233         clientBundle.loadClass("my.client.ClientClass").newInstance();
234         // ... and not hidden the previously default com.sun.xml.internal.bind.v2...
235         clientBundle.loadClass("my.client.OtherClientClass").newInstance();
236         felix.stop();
237     }
238 
239     private Bundle installBootDelegationBundles() throws Exception {
240         // Server class extends JUnit TestCase class, which is not available to the bundle
241         final File pluginServer = new PluginJarBuilder("plugin")
242                 .addResource("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n" +
243                         "Bundle-Version: 1.0\n" +
244                         "Bundle-SymbolicName: my.server\n" +
245                         "Bundle-ManifestVersion: 2\n" +
246                         "Export-Package: my.server\n")
247                 .addJava("my.server.ServerClass", "package my.server; public class ServerClass extends junit.framework.TestCase {}")
248                 .addJava("my.server.OtherServerClass", "package my.server; public class OtherServerClass " +
249                         "extends com.sun.xml.bind.v2.ContextFactory " +
250                         "{}")
251                 .build();
252 
253         // Client is necessary to load the server class in a Felix ContentClassLoader, to avoid the hack in Felix's
254         // R4SearchPolicyCore (approx. line 591) which will use parent delegation if a class cannot be found
255         // and the calling classloader is not a ContentClassLoader.
256         final File pluginClient = new PluginJarBuilder("plugin")
257                 .addResource("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n" +
258                         "Bundle-Version: 1.0\n" +
259                         "Bundle-SymbolicName: my.client\n" +
260                         "Bundle-ManifestVersion: 2\n" +
261                         "Import-Package: my.server\n")
262                 .addJava("my.client.ClientClass", "package my.client; public class ClientClass {" +
263                         "public ClientClass() throws ClassNotFoundException {" +
264                         "getClass().getClassLoader().loadClass(\"my.server.ServerClass\");" +
265                         "}}")
266                 .addJava("my.client.OtherClientClass", "package my.client; public class OtherClientClass {" +
267                         "public OtherClientClass() throws ClassNotFoundException {" +
268                         "getClass().getClassLoader().loadClass(\"my.server.OtherServerClass\");" +
269                         "}}")
270                 .build();
271 
272         final Bundle serverBundle = felix.installBundle(pluginServer, FORBID_REFERENCE);
273         serverBundle.start();
274         final Bundle clientBundle = felix.installBundle(pluginClient, FORBID_REFERENCE);
275         clientBundle.start();
276         return clientBundle;
277     }
278 
279 
280     @Test
281     public void testInstallBundleTwice() throws IOException, BundleException {
282         final File plugin = new PluginJarBuilder("plugin")
283                 .addResource("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n" +
284                         "Import-Package: javax.swing\n" +
285                         "Bundle-Version: 1.0\n" +
286                         "Bundle-SymbolicName: my.foo.symbolicName\n" +
287                         "Bundle-ManifestVersion: 2\n")
288                 .addResource("foo.txt", "foo")
289                 .build();
290 
291         final File pluginUpdate = new PluginJarBuilder("plugin")
292                 .addResource("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n" +
293                         "Import-Package: javax.swing\n" +
294                         "Bundle-Version: 1.0\n" +
295                         "Bundle-SymbolicName: my.foo.symbolicName\n" +
296                         "Bundle-ManifestVersion: 2\n")
297                 .addResource("bar.txt", "bar")
298                 .build();
299 
300         felix.start();
301         assertThat(felix.getBundles(), arrayWithSize(1));
302         final Bundle bundle = felix.installBundle(plugin, FORBID_REFERENCE);
303         assertThat(felix.getBundles(), arrayWithSize(2));
304         assertThat(bundle, bundleWithSymbolicName("my.foo.symbolicName"));
305         assertThat(bundle.getHeaders().get(Constants.BUNDLE_VERSION), is((Object) "1.0"));
306         assertThat(bundle, bundleWithState(Bundle.INSTALLED));
307         assertThat(bundle.getResource("foo.txt"), notNullValue());
308         assertThat(bundle.getResource("bar.txt"), nullValue());
309         bundle.start();
310         assertThat(bundle, bundleWithState(Bundle.ACTIVE));
311         final Bundle bundleUpdate = felix.installBundle(pluginUpdate, FORBID_REFERENCE);
312         assertThat(felix.getBundles(), arrayWithSize(2));
313         assertThat(bundleUpdate, bundleWithState(Bundle.INSTALLED));
314         bundleUpdate.start();
315         assertThat(bundleUpdate, bundleWithState(Bundle.ACTIVE));
316         assertThat(bundleUpdate.getResource("foo.txt"), nullValue());
317         assertThat(bundleUpdate.getResource("bar.txt"), notNullValue());
318     }
319 
320     @Test
321     public void testInstallBundleTwiceDifferentSymbolicNames() throws IOException, BundleException {
322         final File plugin = new PluginJarBuilder("plugin")
323                 .addResource("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n" +
324                         "Import-Package: javax.swing\n" +
325                         "Bundle-Version: 1.0\n" +
326                         "Bundle-SymbolicName: my.foo\n" +
327                         "Atlassian-Plugin-Key: my.foo.symbolicName\n" +
328                         "Bundle-ManifestVersion: 2\n")
329                 .addResource("foo.txt", "foo")
330                 .build();
331 
332         final File pluginUpdate = new PluginJarBuilder("plugin")
333                 .addResource("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n" +
334                         "Import-Package: javax.swing\n" +
335                         "Bundle-Version: 1.0\n" +
336                         "Atlassian-Plugin-Key: my.foo.symbolicName\n" +
337                         "Bundle-SymbolicName: my.bar\n" +
338                         "Bundle-ManifestVersion: 2\n")
339                 .addResource("bar.txt", "bar")
340                 .build();
341 
342         felix.start();
343         assertThat(felix.getBundles(), arrayWithSize(1));
344         final Bundle bundle = felix.installBundle(plugin, FORBID_REFERENCE);
345         assertThat(felix.getBundles(), arrayWithSize(2));
346         assertThat(bundle.getHeaders().get(Constants.BUNDLE_VERSION), is((Object) "1.0"));
347         assertThat(bundle, bundleWithState(Bundle.INSTALLED));
348         assertThat(bundle.getResource("foo.txt"), notNullValue());
349         assertThat(bundle.getResource("bar.txt"), nullValue());
350         bundle.start();
351         assertThat(bundle, bundleWithState(Bundle.ACTIVE));
352         final Bundle bundleUpdate = felix.installBundle(pluginUpdate, FORBID_REFERENCE);
353         assertThat(felix.getBundles(), arrayWithSize(2));
354         assertThat(bundleUpdate, bundleWithState(Bundle.INSTALLED));
355         bundleUpdate.start();
356         assertThat(bundleUpdate, bundleWithState(Bundle.ACTIVE));
357         assertThat(bundleUpdate.getResource("foo.txt"), nullValue());
358         assertThat(bundleUpdate.getResource("bar.txt"), notNullValue());
359     }
360 
361     @Test
362     public void testInstallFailure() throws Exception {
363         final File plugin = new PluginJarBuilder("plugin")
364                 .addResource("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n" +
365                         "Bundle-Version: 1.0\n" +
366                         "Import-Package: foo.missing.package\n" +
367                         "Bundle-SymbolicName: my.foo.symbolicName\n" +
368                         "Bundle-ManifestVersion: 2\n")
369                 .build();
370         felix.start();
371 
372         final Bundle bundle = felix.installBundle(plugin, FORBID_REFERENCE);
373         try {
374             bundle.loadClass("foo.bar");
375             fail("Should have thrown exception");
376         } catch (final ClassNotFoundException ex) {
377             // no worries
378         }
379     }
380 
381     @Test
382     public void testServiceTrackerIsClosed() {
383         felix.start();
384         // PackageAdmin is an example of service which is loaded by default in Felix
385         final ServiceTracker tracker = felix.getServiceTracker(PackageAdmin.class.getCanonicalName());
386         Object[] trackedServices = tracker.getServices();
387         assertThat(trackedServices, notNullValue());
388 
389         tracker.close();
390 
391         trackedServices = tracker.getServices();
392         assertThat(trackedServices, nullValue());
393     }
394 
395     @Test
396     public void testServiceTrackerActuallyTracksStuff() throws Exception {
397         startFelixWithListAsOSGiService();
398 
399         final ServiceTracker tracker = felix.getServiceTracker(List.class.getName());
400         final Object[] trackedServices = tracker.getServices();
401 
402         // Combining these into a single match is nontrivial on account of needing to manage the types.
403         assertThat(trackedServices, arrayWithSize(1));
404         assertThat(trackedServices[0], is(ImmutableList.of("blah")));
405     }
406 
407     @Test
408     public void testServiceTrackerCutomizerIsInPlace() throws Exception {
409         startFelixWithListAsOSGiService();
410         final CountDownLatch latch = new CountDownLatch(1);
411 
412         felix.getServiceTracker(List.class.getName(), new ServiceTrackerCustomizer() {
413             public Object addingService(final ServiceReference reference) {
414                 latch.countDown();
415                 return reference;
416             }
417 
418             public void modifiedService(final ServiceReference reference, final Object service) {
419             }
420 
421             public void removedService(final ServiceReference reference, final Object service) {
422             }
423         });
424         assertThat(latch.await(10, TimeUnit.SECONDS), is(true));
425     }
426 
427     private void startFelixWithListAsOSGiService() throws Exception {
428         final File plugin = new PluginJarBuilder("plugin")
429                 .addResource("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n" +
430                         "Bundle-Version: 1.0\n" +
431                         "Atlassian-Plugin-Key: my.foo.symbolicName\n" +
432                         "Bundle-Name: my.bar\n" +
433                         "Bundle-SymbolicName: my.bar\n" +
434                         "Bundle-ManifestVersion: 2\n" +
435                         "Bundle-Activator: my.MyActivator\n" +
436                         "Import-Package: org.osgi.framework\n")
437                 .addJava("my.MyActivator", "package my;" +
438                         "import org.osgi.framework.ServiceRegistration;\n" +
439                         "import org.osgi.framework.BundleActivator;\n" +
440                         "import org.osgi.framework.BundleContext;\n" +
441                         "import java.util.*;\n" +
442                         "public class MyActivator implements BundleActivator {\n" +
443                         "    private ServiceRegistration registration;\n" +
444                         "    public void start(BundleContext context) throws Exception\n" +
445                         "    {\n" +
446                         "        context.registerService(List.class.getName(), Arrays.asList(new Object[]{ \"blah\" }), new Properties());\n" +
447                         "    }\n" +
448                         "    public void stop(BundleContext context) throws Exception {}\n" +
449                         "}")
450                 .build();
451         felix.start();
452         final Bundle bundle = felix.installBundle(plugin, FORBID_REFERENCE);
453         bundle.start();
454     }
455 
456     @Test
457     public void testRuntimeEnvironment() {
458         final int timeout = 3883;
459         setPluginTimeout(timeout);
460         final String expectedRuntimeEnvironment = String.format(
461                 "java.version=%s,plugin.enable.timeout=%d",
462                 System.getProperty("java.version"), timeout);
463 
464         assertThat(felix.getRuntimeEnvironment(), is(expectedRuntimeEnvironment));
465     }
466 
467     @Test
468     public void testRuntimeEnvironmentIntegration() throws Exception {
469         final int timeout = 678;
470         setPluginTimeout(timeout);
471 
472         startFelixWithListAsOSGiService();
473 
474         final File versionFile = new File(new File(tmpdir, "transformed-plugins"), "cache.key");
475         assertThat(versionFile.exists(), is(true));
476         final String txt = FileUtils.readFileToString(versionFile);
477 
478         final ExportsBuilder eBuilder = new ExportsBuilder();
479         final String systemExports = eBuilder.getExports(new ArrayList<>(),
480                 new DefaultPackageScannerConfiguration());
481 
482         // cache key should include the current JVM version
483         final String expectedKey = String.format(
484                 "java.version=%s,plugin.enable.timeout=%d,%s",
485                 System.getProperty("java.version"),
486                 timeout,
487                 systemExports);
488         assertThat(txt, is(Hashing.sha1().hashString(expectedKey, Charsets.UTF_8).toString()));
489     }
490 
491     @Test
492     public void testFrameworkBundlesUnzipped() {
493         // Check that nothing.zip, at frameworkBundlesUrl is unpacked when the framework is started
494         // Should start empty or the test is broken
495         assertThat(osgiPersistentCache.getFrameworkBundleCache().listFiles(), emptyArray());
496         felix.start();
497         assertThat(osgiPersistentCache.getFrameworkBundleCache().listFiles(), arrayContaining(fileNamed("nothing.txt")));
498     }
499 
500     @Test
501     public void testFrameworkBundleDirectorySkipsUnzip() throws Exception {
502         final File frameworkBundles = new File(tmpdir, "provided-framework-bundles");
503         FileUtils.forceMkdir(frameworkBundles);
504         final String symbolicName = "my.foo.frameworkbundle";
505         final Map<String, String> manifest = ImmutableMap.<String, String>builder()
506                 .put("Manifest-Version", "1.0")
507                 .put(BUNDLE_NAME, "Framework Bundle")
508                 .put(BUNDLE_SYMBOLICNAME, symbolicName)
509                 .build();
510         new PluginJarBuilder("framework-bundle")
511                 .manifest(manifest)
512                 .build(frameworkBundles);
513         felix = new FelixOsgiContainerManager(
514                 frameworkBundles,
515                 osgiPersistentCache,
516                 new DefaultPackageScannerConfiguration(),
517                 null,
518                 new DefaultPluginEventManager());
519 
520         felix.start();
521         // Even after start, the framework bundle cache should be empty
522         assertThat(osgiPersistentCache.getFrameworkBundleCache().listFiles(), emptyArray());
523         // And we should have found the bundle we put in the provided directory
524         assertThat(felix.getBundles(), hasItemInArray(bundleWithSymbolicName(symbolicName)));
525     }
526 
527     @Test
528     public void testFrameworkBundleDirectoryDoesNotExistThrowsNiceException() {
529         final File frameworkBundles = new File(tmpdir, "framework-bundles-dir-does-not-exist");
530         felix = new FelixOsgiContainerManager(
531                 frameworkBundles,
532                 osgiPersistentCache,
533                 new DefaultPackageScannerConfiguration(),
534                 null,
535                 new DefaultPluginEventManager());
536 
537         TestFelixLogger logger = new TestFelixLogger();
538         logger.setLogLevel(Logger.LOG_WARNING);
539         felix.setFelixLogger(logger);
540 
541         assertThat(frameworkBundles.exists(), is(false));
542         felix.start();
543 
544         assertThat(logger.getExceptions(), hasSize(1));
545         //noinspection ThrowableResultOfMethodCallIgnored
546         Throwable exception = logger.getExceptions().get(0);
547         Matcher<String> matchesException = allOf(containsString("BundleException"), containsString(frameworkBundles.toString()));
548         assertThat(exception.toString(), matchesException);
549     }
550 
551     private void setPluginTimeout(final int timeout) {
552         System.setProperty(ATLASSIAN_PLUGINS_ENABLE_WAIT, String.valueOf(timeout));
553     }
554 
555     /**
556      * Matches a Bundle whose location indicates that it is a reference bundle.
557      *
558      * @return a matcher for bundles whose location is a reference: location.
559      */
560     private static Matcher<Bundle> bundleThatIsReference() {
561         return bundleWithLocation(startsWith(BundleArchive.REFERENCE_PROTOCOL));
562     }
563 
564     /**
565      * Matches a Bundle whose location indicates that it is a reference bundle.
566      *
567      * @return a matcher for bundles whose location is a reference: location.
568      */
569     private static Matcher<Bundle> bundleWithLocation(final Matcher<String> locationMatcher) {
570         return new FeatureMatcher<Bundle, String>(locationMatcher, "bundle with location", "bundle location") {
571             @Override
572             protected String featureValueOf(final Bundle bundle) {
573                 return bundle.getLocation();
574             }
575         };
576     }
577 
578     /**
579      * Matches a Bundle whose symbolic name is as given.
580      *
581      * @param symbolicName the expected symbolic name for the bundle.
582      * @return a matcher for bundles whose symbolic name is {@code symbolicName}.
583      */
584     private static Matcher<Bundle> bundleWithSymbolicName(final String symbolicName) {
585         return new FeatureMatcher<Bundle, String>(equalTo(symbolicName), "bundle with symbolic name", "symbolic name") {
586             @Override
587             protected String featureValueOf(final Bundle bundle) {
588                 return bundle.getSymbolicName();
589             }
590         };
591     }
592 
593     /**
594      * Matches a Bundle whose state is as given.
595      *
596      * @param bundleState the expected state for the bundle.
597      * @return a matcher for bundles whose state is {@code bundleState}.
598      */
599     private static Matcher<Bundle> bundleWithState(final Integer bundleState) {
600         return new FeatureMatcher<Bundle, Integer>(equalTo(bundleState), "a bundle with state", "bundle state") {
601             @Override
602             protected Integer featureValueOf(final Bundle bundle) {
603                 return bundle.getState();
604             }
605         };
606     }
607 
608     private static class TestFelixLogger extends Logger {
609 
610         private final List<Throwable> exceptions = new ArrayList<>();
611 
612         @Override
613         protected void doLog(int level, String msg, Throwable throwable) {
614             super.doLog(level, msg, throwable);
615             exceptions.add(throwable);
616         }
617 
618         List<Throwable> getExceptions() {
619             return exceptions;
620         }
621     }
622 }