View Javadoc
1   package com.atlassian.plugin.loaders;
2   
3   import com.atlassian.plugin.ModuleDescriptorFactory;
4   import com.atlassian.plugin.Plugin;
5   import com.atlassian.plugin.PluginArtifact;
6   import com.atlassian.plugin.PluginException;
7   import com.atlassian.plugin.PluginInternal;
8   import com.atlassian.plugin.ReferenceMode;
9   import com.atlassian.plugin.event.PluginEventManager;
10  import com.atlassian.plugin.factories.PluginFactory;
11  import com.atlassian.plugin.loaders.classloading.DeploymentUnit;
12  import com.atlassian.plugin.loaders.classloading.Scanner;
13  import com.atlassian.plugin.test.CapturedLogging;
14  import com.atlassian.plugin.test.PluginJarBuilder;
15  import com.atlassian.plugin.test.PluginTestUtils;
16  import com.google.common.base.Function;
17  import com.google.common.collect.Iterables;
18  import com.google.common.collect.Lists;
19  import org.apache.commons.io.FileUtils;
20  import org.junit.After;
21  import org.junit.Before;
22  import org.junit.Rule;
23  import org.junit.Test;
24  import org.junit.rules.ExpectedException;
25  import org.mockito.ArgumentCaptor;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.net.URL;
30  import java.util.Arrays;
31  import java.util.Collection;
32  import java.util.List;
33  
34  import static com.atlassian.plugin.test.CapturedLogging.didLogWarn;
35  import static org.hamcrest.Matchers.containsInAnyOrder;
36  import static org.hamcrest.Matchers.instanceOf;
37  import static org.hamcrest.Matchers.is;
38  import static org.junit.Assert.assertEquals;
39  import static org.junit.Assert.assertThat;
40  import static org.junit.Assert.assertTrue;
41  import static org.mockito.ArgumentMatchers.isA;
42  import static org.mockito.ArgumentMatchers.same;
43  import static org.mockito.Mockito.mock;
44  import static org.mockito.Mockito.verify;
45  import static org.mockito.Mockito.when;
46  
47  public class TestBundledPluginLoader {
48  
49      @Rule
50      public final ExpectedException expectedException = ExpectedException.none();
51      @Rule
52      public final CapturedLogging capturedLogging = new CapturedLogging(BundledPluginLoader.class);
53  
54      private File pluginDir;
55  
56      private static final String fooXml = "foo.xml";
57  
58      @Before
59      public void createTemporaryDirectory() throws IOException {
60          pluginDir = PluginTestUtils.createTempDirectory(TestBundledPluginLoader.class);
61      }
62  
63      @After
64      public void deleteTemporaryDirectory() throws Exception {
65          FileUtils.deleteDirectory(pluginDir);
66          pluginDir = null;
67      }
68  
69      @Test
70      public void loaderFromZipWithUrlConstructorContainsExpectedFiles() throws IOException {
71          final File bundledZip = buildBundledZip();
72          final BundledPluginLoader loader = buildBundledPluginLoader(bundledZip, true);
73          assertLoaderContains(loader, fooXml);
74      }
75  
76      @Test
77      public void loaderFromZipWithNonUrlConstructorContainsExpectedFiles() throws IOException {
78          final File bundledZip = buildBundledZip();
79          final BundledPluginLoader loader = buildBundledPluginLoader(bundledZip, false);
80          // Yes, this will be empty. For a zip, we must use the constructor that can specify a
81          // directory. However, the code logs and returns and empty loader rather than throwing,
82          // because this is behaviour that will make the jira change most compatible.
83          assertLoaderContains(loader);
84      }
85  
86      @Test
87      public void loaderFromDirectoryWithUrlConstructorContainsExpectedFiles() throws IOException {
88          validateLoaderFromDirectoryContainsExpectedFiles(true);
89      }
90  
91      @Test
92      public void loaderFromDirectoryWithNonUrlConstructorContainsExpectedFiles() throws IOException {
93          validateLoaderFromDirectoryContainsExpectedFiles(false);
94      }
95  
96      private void validateLoaderFromDirectoryContainsExpectedFiles(final boolean useUrlConstructor) throws IOException {
97          final File dir = PluginTestUtils.createTempDirectory(TestBundledPluginLoader.class);
98          FileUtils.writeStringToFile(new File(dir, "foo.txt"), "hello");
99  
100         final BundledPluginLoader loader = buildBundledPluginLoader(dir, useUrlConstructor);
101         assertLoaderContains(loader, "foo.txt");
102     }
103 
104     @Test
105     public void loaderFromFileListWithUrlConstructorContainsExpectedFiles() throws IOException {
106         validateLoaderFromFileListContainsExpectedFiles(true);
107     }
108 
109     @Test
110     public void loaderFromFileListWithNonUrlConstructorContainsExpectedFiles() throws IOException {
111         validateLoaderFromFileListContainsExpectedFiles(false);
112     }
113 
114     private void validateLoaderFromFileListContainsExpectedFiles(final boolean useUrlConstructor) throws IOException {
115         FileUtils.writeStringToFile(new File(pluginDir, "foo.txt"), "hello");
116         FileUtils.writeStringToFile(new File(pluginDir, "bar.txt"), "world");
117         final File listFile = new File(pluginDir, "bundled-plugins" + BundledPluginLoader.getListSuffix());
118         FileUtils.writeStringToFile(listFile, "foo.txt\nbar.txt");
119 
120         final BundledPluginLoader loader = buildBundledPluginLoader(listFile, useUrlConstructor);
121         assertLoaderContains(loader, "foo.txt", "bar.txt");
122     }
123 
124     @Test
125     public void loaderFromUnsupportedFileWithUrlConstructorContainsExpectedFiles() throws IOException {
126         validateLoaderFromUnsupportedFileContainsExpectedFiles(true);
127     }
128 
129     @Test
130     public void loaderFromUnsupportedFileWithNonUrlConstructorContainsExpectedFiles() throws IOException {
131         validateLoaderFromUnsupportedFileContainsExpectedFiles(false);
132     }
133 
134     private void validateLoaderFromUnsupportedFileContainsExpectedFiles(final boolean useUrlConstructor) throws IOException {
135         final File unsupportedFile = new File(pluginDir, "notASuitableFile.unknown-suffix");
136         FileUtils.writeStringToFile(unsupportedFile, "Some\nRandom\nContent\n");
137 
138         final BundledPluginLoader loader = buildBundledPluginLoader(unsupportedFile, useUrlConstructor);
139         assertLoaderContains(loader);
140     }
141 
142     @Test
143     public void loaderScannerDoesNotRemoveUnderlyingFiles() throws IOException {
144         final File bundledZip = buildBundledZip();
145 
146         // Since it's actually a zip, we must use the urlConstructor (hence true)
147         final BundledPluginLoader loader = buildBundledPluginLoader(bundledZip, true);
148         final Scanner scanner = loader.scanner;
149         assertLoaderContains(loader, fooXml);
150         final Collection<DeploymentUnit> units = scanner.getDeploymentUnits();
151         final int countBefore = units.size();
152         int found = 0;
153         for (final DeploymentUnit unit : units) {
154             final File file = unit.getPath();
155             if (fooXml.equals(file.getName())) {
156                 found += 1;
157                 assertTrue(file.exists());
158                 scanner.remove(unit);
159                 // Yes, it should still exist
160                 assertTrue(file.exists());
161             }
162         }
163         // We should have found a foo.txt
164         assertEquals(1, found);
165         // We should not have actually removed it from the scanner
166         assertEquals(countBefore, scanner.getDeploymentUnits().size());
167     }
168 
169     @Test
170     public void loaderRemoveDoesUninstallPlugin() throws IOException {
171         final File bundledZip = buildBundledZip();
172         final PluginInternal mockPlugin = mock(PluginInternal.class);
173         final PluginFactory pluginFactory = mock(PluginFactory.class);
174         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
175 
176         final String key = "plugin.key.foo";
177         final ArgumentCaptor<PluginArtifact> pluginArtifact = ArgumentCaptor.forClass(PluginArtifact.class);
178 
179         when(mockPlugin.getKey()).thenReturn(key);
180         when(mockPlugin.isDeleteable()).thenReturn(true);
181         when(mockPlugin.isUninstallable()).thenReturn(true);
182         when(pluginFactory.canCreate(isA(PluginArtifact.class))).thenReturn(key);
183         when(pluginFactory.create(pluginArtifact.capture(), same(moduleDescriptorFactory))).thenReturn(mockPlugin);
184 
185         // Since it's actually a zip, we must use the urlConstructor (hence true)
186         final BundledPluginLoader loader = buildBundledPluginLoader(bundledZip, true, pluginFactory);
187         final Collection<Plugin> plugins = Lists.newArrayList(loader.loadAllPlugins(moduleDescriptorFactory));
188         assertEquals(1, plugins.size());
189         // Note that the plugin we get back is actually wrapped, so we don't test equality
190         final Plugin bundledPlugin = Iterables.getOnlyElement(plugins);
191         assertEquals(key, bundledPlugin.getKey());
192         assertTrue(bundledPlugin.isDeleteable());
193         verify(mockPlugin).isDeleteable();
194         verify(mockPlugin).setBundledPlugin(true);
195 
196         // Remove it, and check Plugin was uninstalled
197         loader.removePlugin(bundledPlugin);
198         verify(mockPlugin).isUninstallable();
199         verify(mockPlugin).uninstall();
200         // Check file on disk didn't get removed
201         assertTrue(pluginArtifact.getValue().toFile().exists());
202 
203         // There's no way to query a loader for its plugins, so we try to unload again and look for
204         // the exception. This currently throws because the Plugin is gone. This is a little
205         // brittle, because code evolution in the test subject means it might throw because the
206         // Plugin state changed, but since the Plugin is a mock, it's state doesn't change without
207         // us asking. So it's likely to be good. We could check the exception text, but i reckon
208         // this is even more brittle.
209         expectedException.expect(PluginException.class);
210         loader.removePlugin(bundledPlugin);
211     }
212 
213     @Test
214     public void pluginArtifactsAllowReference() throws IOException {
215         // Use a list file for the bundled plugins to make it easy to get a jar filename in there
216         final File dir = PluginTestUtils.createTempDirectory(TestBundledPluginLoader.class);
217         final File bundleList = new File(dir, "bundled-plugins" + BundledPluginLoader.getListSuffix());
218         FileUtils.writeStringToFile(bundleList, "some.jar\n");
219         final Plugin mockPlugin = mock(Plugin.class);
220         final PluginFactory pluginFactory = mock(PluginFactory.class);
221         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
222 
223         final String key = "plugin.key.foo";
224         final ArgumentCaptor<PluginArtifact> pluginArtifactCaptor = ArgumentCaptor.forClass(PluginArtifact.class);
225 
226         when(mockPlugin.getKey()).thenReturn(key);
227         when(pluginFactory.canCreate(isA(PluginArtifact.class))).thenReturn(key);
228         when(pluginFactory.create(pluginArtifactCaptor.capture(), same(moduleDescriptorFactory))).thenReturn(mockPlugin);
229 
230         // We could use either constructor, but non url is recommended for non zip source.
231         final BundledPluginLoader loader = buildBundledPluginLoader(bundleList, false, pluginFactory);
232         loader.loadAllPlugins(moduleDescriptorFactory);
233         final PluginArtifact pluginArtifact = pluginArtifactCaptor.getValue();
234         assertThat(pluginArtifact.getReferenceMode(), is(ReferenceMode.PERMIT_REFERENCE));
235     }
236 
237     @Test
238     public void postProcessWarnsAboutInvalidPlugin() throws IOException {
239         final Plugin plugin = mock(Plugin.class);
240         when(plugin.toString()).thenReturn("blargh");
241 
242         final BundledPluginLoader bundledPluginLoader = buildBundledPluginLoader(buildBundledZip(), true);
243 
244         bundledPluginLoader.postProcess(plugin);
245         assertThat(capturedLogging, didLogWarn("blargh", Plugin.class.getCanonicalName()));
246     }
247 
248     private File buildBundledZip() throws IOException {
249         return new PluginJarBuilder("bundledPlugins")
250                 .addResource(fooXml, "<foo/>")
251                 .buildWithNoManifest();
252     }
253 
254     private BundledPluginLoader buildBundledPluginLoader(
255             final File bundledPlugins,
256             final boolean useUrlConstructor,
257             final PluginFactory... pluginFactories)
258             throws IOException {
259         final List<PluginFactory> pluginFactoryList = Arrays.asList(pluginFactories);
260         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
261         if (useUrlConstructor) {
262             final URL bundledPluginsUrl = bundledPlugins.toURI().toURL();
263             return new BundledPluginLoader(bundledPluginsUrl, pluginDir, pluginFactoryList, pluginEventManager);
264         } else {
265             return new BundledPluginLoader(bundledPlugins, pluginFactoryList, pluginEventManager);
266         }
267     }
268 
269     private void assertLoaderContains(final BundledPluginLoader loader, final String... expectedEntries) {
270         final Collection<DeploymentUnit> scanned = loader.scanner.scan();
271         final Iterable<String> actualEntries = Iterables.transform(scanned, new Function<DeploymentUnit, String>() {
272             @Override
273             public String apply(final DeploymentUnit unit) {
274                 return unit.getPath().getName();
275             }
276         });
277         assertThat(actualEntries, containsInAnyOrder(expectedEntries));
278     }
279 }