View Javadoc
1   package com.atlassian.plugin.osgi.factory.transform;
2   
3   import com.atlassian.plugin.JarPluginArtifact;
4   import com.atlassian.plugin.PluginAccessor;
5   import com.atlassian.plugin.osgi.container.OsgiContainerManager;
6   import com.atlassian.plugin.osgi.container.impl.DefaultOsgiPersistentCache;
7   import com.atlassian.plugin.osgi.factory.transform.model.SystemExports;
8   import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
9   import com.atlassian.plugin.osgi.hostcomponents.PropertyBuilder;
10  import com.atlassian.plugin.osgi.hostcomponents.impl.MockRegistration;
11  import com.atlassian.plugin.test.PluginJarBuilder;
12  import com.atlassian.plugin.test.PluginTestUtils;
13  import com.google.common.collect.ImmutableMap;
14  import com.google.common.collect.Sets;
15  import org.dom4j.Document;
16  import org.dom4j.Element;
17  import org.dom4j.io.SAXReader;
18  import org.jaxen.SimpleNamespaceContext;
19  import org.jaxen.XPath;
20  import org.jaxen.dom4j.Dom4jXPath;
21  import org.junit.After;
22  import org.junit.Before;
23  import org.junit.Test;
24  import org.osgi.framework.Constants;
25  import org.osgi.framework.ServiceReference;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import java.io.File;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.net.URISyntaxException;
33  import java.nio.file.Files;
34  import java.nio.file.Paths;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.Enumeration;
38  import java.util.HashMap;
39  import java.util.HashSet;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Set;
43  import java.util.jar.Attributes;
44  import java.util.jar.JarEntry;
45  import java.util.jar.JarFile;
46  import java.util.zip.Deflater;
47  import java.util.zip.ZipEntry;
48  import java.util.zip.ZipFile;
49  
50  import static org.hamcrest.MatcherAssert.assertThat;
51  import static org.hamcrest.Matchers.containsString;
52  import static org.hamcrest.Matchers.endsWith;
53  import static org.hamcrest.Matchers.greaterThanOrEqualTo;
54  import static org.hamcrest.Matchers.is;
55  import static org.hamcrest.Matchers.lessThan;
56  import static org.hamcrest.Matchers.not;
57  import static org.hamcrest.core.IsNull.notNullValue;
58  import static org.mockito.Mockito.mock;
59  import static org.mockito.Mockito.when;
60  
61  public class TestDefaultPluginTransformer {
62      private static final Logger LOG = LoggerFactory.getLogger(TestDefaultPluginTransformer.class);
63  
64      private static final Map<String, String> BEAN_NAMESPACE = ImmutableMap.<String, String>builder().put("beans", "http://www.springframework.org/schema/beans").build();
65      private static final Map<String, String> OSGI_NAMESPACE = ImmutableMap.<String, String>builder().put("osgi", "http://www.eclipse.org/gemini/blueprint/schema/blueprint").build();
66  
67      private static final String BEAN_XPATH = "//beans:bean";
68      private static final String OSGI_REF_XPATH = "//osgi:reference";
69  
70      private DefaultPluginTransformer transformer;
71      private File tmpDir;
72  
73      @Before
74      public void setUp() throws Exception {
75          OsgiContainerManager osgiContainerManager = mock(OsgiContainerManager.class);
76          when(osgiContainerManager.getRegisteredServices()).thenReturn(new ServiceReference[0]);
77          tmpDir = PluginTestUtils.createTempDirectory("plugin-transformer");
78          transformer = new DefaultPluginTransformer(new DefaultOsgiPersistentCache(tmpDir), SystemExports.NONE, null, PluginAccessor.Descriptor.FILENAME, osgiContainerManager);
79      }
80  
81      @After
82      public void tearDown() throws Exception {
83          tmpDir = null;
84          transformer = null;
85      }
86  
87      @Test
88      public void testAddFilesToZip() throws URISyntaxException, IOException {
89          final File file = PluginTestUtils.getFileForResource("myapp-1.0-plugin.jar");
90  
91          final Map<String, byte[]> files = new HashMap<String, byte[]>() {
92              {
93                  put("foo", "bar".getBytes());
94              }
95          };
96          final File copy = transformer.addFilesToExistingZip(file, files);
97          assertThat(copy, notNullValue());
98          assertThat(copy.getName(), not(is(file.getName())));
99          assertThat(copy.length(), not(is(file.length())));
100 
101         try (final ZipFile zip = new ZipFile(copy)) {
102             final ZipEntry entry = zip.getEntry("foo");
103             assertThat(entry, notNullValue());
104         }
105     }
106 
107     @Test
108     public void testExistingFilesRetainTimestamps() throws URISyntaxException, IOException {
109         // When a plugin is transformed, all existing files should retain the same
110         // modified timestamp from the source jar
111         // (also checks they share the same ordering)
112 
113         final File expectedSource = PluginTestUtils.getFileForResource("myapp-1.0-plugin.jar");
114         final File actualSource = PluginTestUtils.getFileForResource("myapp-1.0-plugin.jar");
115         final Map<String, byte[]> files = new HashMap<>();
116 
117 
118         try (final ZipFile expectedZip = new ZipFile(expectedSource);
119              final ZipFile actualZip = new ZipFile(transformer.addFilesToExistingZip(actualSource, files))) {
120             Enumeration<? extends ZipEntry> expectedEntries = expectedZip.entries();
121             Enumeration<? extends ZipEntry> actualEntries = actualZip.entries();
122 
123             while (expectedEntries.hasMoreElements()) {
124                 ZipEntry expected = expectedEntries.nextElement();
125                 ZipEntry actual = actualEntries.nextElement();
126                 assertThat(actual.getName(), is(expected.getName()));
127                 assertThat(actual.getTime(), is(expected.getTime()));
128             }
129         }
130     }
131 
132     @Test
133     public void testFilesAddedToZipAreInOrder() throws URISyntaxException, IOException {
134         // Validate that any custom files added to the zip are present in
135         // sorted order (sorted by filename asc)
136         // We need this to keep the output jar deterministic
137 
138         final File file = PluginTestUtils.getFileForResource("myapp-1.0-plugin.jar");
139 
140         final Map<String, byte[]> files = new HashMap<String, byte[]>() {
141             {
142                 put("testcode/ghi", "aaa".getBytes());
143                 put("testcode/abc", "aaa".getBytes());
144                 put("testcode/dir/def", "aaa".getBytes());
145                 put("testcode/dir/abc", "aaa".getBytes());
146             }
147         };
148 
149         List<String> expectedFileOrder = Arrays.asList("testcode/abc", "testcode/dir/abc", "testcode/dir/def", "testcode/ghi");
150         List<String> actualFileOrder = new ArrayList<>();
151 
152         try (final ZipFile zip = new ZipFile(transformer.addFilesToExistingZip(file, files))) {
153             Enumeration<? extends ZipEntry> entries = zip.entries();
154             while (entries.hasMoreElements()) {
155                 ZipEntry entry = entries.nextElement();
156                 if (entry.getName().contains("testcode")) {
157                     actualFileOrder.add(entry.getName());
158                 }
159             }
160 
161             assertThat(actualFileOrder.size(), is(expectedFileOrder.size()));
162             assertThat(actualFileOrder, is(expectedFileOrder));
163         }
164     }
165 
166     @Test
167     public void testTransform() throws Exception {
168         final File file = new PluginJarBuilder()
169                 .addFormattedJava("my.Foo",
170                         "package my;",
171                         "public class Foo {",
172                         "  com.atlassian.plugin.osgi.factory.transform.Fooable bar;",
173                         "}")
174                 .addPluginInformation("foo", "foo", "1.1")
175                 .build();
176 
177         final File copy = transformer.transform(new JarPluginArtifact(file), new ArrayList<HostComponentRegistration>() {
178             {
179                 add(new StubHostComponentRegistration(Fooable.class));
180             }
181         });
182 
183         assertThat(copy, notNullValue(File.class));
184         assertThat(copy.getName().contains(String.valueOf(file.lastModified())), is(true));
185         assertThat(copy.getName().endsWith(".jar"), is(true));
186         assertThat(copy.getParentFile().getParentFile().getAbsolutePath(), is(tmpDir.getAbsolutePath()));
187 
188         try (JarFile jar = new JarFile(copy)) {
189             final Attributes attrs = jar.getManifest().getMainAttributes();
190             assertThat(attrs.getValue(Constants.BUNDLE_VERSION), is("1.1"));
191             assertThat(jar.getEntry("META-INF/spring/atlassian-plugins-host-components.xml"), notNullValue());
192         }
193     }
194 
195     @Test
196     public void testImportManifestGenerationOnInterfaces() throws Exception {
197         final File innerJar = new PluginJarBuilder()
198                 .addFormattedJava("my.innerpackage.InnerPackageInterface1",
199                         "package my.innerpackage;",
200                         "public interface InnerPackageInterface1 {}")
201                 .build();
202 
203         final File pluginJar = new PluginJarBuilder()
204                 .addFormattedJava("my.MyFooChild",
205                         "package my;",
206                         "public class MyFooChild extends com.atlassian.plugin.osgi.factory.transform.dummypackage2.DummyClass2 {",
207                         "}")
208                 .addFormattedJava("my2.MyFooInterface",
209                         "package my2;",
210                         "public interface MyFooInterface {}")
211                 .addFormattedResource("atlassian-plugin.xml",
212                         "<atlassian-plugin name='plugin1' key='first' pluginsVersion='2'>",
213                         "    <plugin-info>",
214                         "        <version>1.0</version>",
215                         "    </plugin-info>",
216                         "    <component key='component1' class='my.MyFooChild' public='true'>",
217                         "       <interface>com.atlassian.plugin.osgi.factory.transform.dummypackage0.DummyInterface0</interface>",
218                         "       <interface>com.atlassian.plugin.osgi.factory.transform.dummypackage1.DummyInterface1</interface>",
219                         "       <interface>my.innerpackage.InnerPackageInterface1</interface>",
220                         "       <interface>my2.MyFooInterface</interface>",
221                         "    </component>",
222                         "</atlassian-plugin>")
223                 .addFile("META-INF/lib/mylib.jar", innerJar)
224                 .build();
225 
226         File outputFile = transformer.transform(new JarPluginArtifact(pluginJar), new ArrayList<HostComponentRegistration>());
227 
228         String importString;
229         try (JarFile outputJar = new JarFile(outputFile)) {
230             importString = outputJar.getManifest().getMainAttributes().getValue(Constants.IMPORT_PACKAGE);
231         }
232 
233         // this should be done by binary scanning.
234         assertThat(importString, containsString("com.atlassian.plugin.osgi.factory.transform.dummypackage2"));
235 
236         // referred to by interface declaration.
237         assertThat(importString, containsString("com.atlassian.plugin.osgi.factory.transform.dummypackage1"));
238 
239         // referred to by interface declaration
240         assertThat(importString, containsString("com.atlassian.plugin.osgi.factory.transform.dummypackage0"));
241 
242         // should not import an interface which exists in plugin itself.
243         assertThat(importString, not(containsString("my2.MyFooInterface")));
244 
245         // should not import an interface which exists in inner jar.
246         assertThat(importString, not(containsString("my.innerpackage")));
247     }
248 
249     @Test
250     public void testGenerateCacheName() throws IOException {
251         File tmp = File.createTempFile("asdf", ".jar", tmpDir);
252         assertThat(DefaultPluginTransformer.generateCacheName(tmp), endsWith(".jar"));
253         tmp = File.createTempFile("asdf", "asdf", tmpDir);
254         assertThat(DefaultPluginTransformer.generateCacheName(tmp), endsWith(String.valueOf(tmp.lastModified())));
255 
256         tmp = File.createTempFile("asdf", "asdf.", tmpDir);
257         assertThat(DefaultPluginTransformer.generateCacheName(tmp), endsWith(String.valueOf(tmp.lastModified())));
258 
259         tmp = File.createTempFile("asdf", "asdf.s", tmpDir);
260         assertThat(DefaultPluginTransformer.generateCacheName(tmp), endsWith(".s"));
261     }
262 
263     @Test
264     public void testTransformComponentMustNotPerformKeyConversion() throws Exception {
265         File outputJarFile = runTransform();
266 
267         assertBeanNames(outputJarFile, "META-INF/spring/atlassian-plugins-components.xml",
268                 BEAN_NAMESPACE, BEAN_XPATH,
269                 Sets.newHashSet("TESTING1", "testing2", "testing3"));
270     }
271 
272     @Test
273     public void testBeansListedInAppearanceOrder() throws Exception {
274         // These beans should appear in the order they appear in atlassian-plugin.xml
275 
276         File outputJarFile = runTransform();
277 
278         List<String> actual = runXpath(outputJarFile, "META-INF/spring/atlassian-plugins-components.xml", BEAN_NAMESPACE, BEAN_XPATH);
279         List<String> expected = Arrays.asList("testing3", "TESTING1", "testing2");
280         assertThat(actual, is(expected));
281     }
282 
283     @Test
284     public void testTransformImportMustNotPerformKeyConversion() throws Exception {
285         File outputJarFile = runTransform();
286 
287         assertBeanNames(outputJarFile, "META-INF/spring/atlassian-plugins-component-imports.xml",
288                 OSGI_NAMESPACE, OSGI_REF_XPATH,
289                 Sets.newHashSet("TESTING3", "testing4"));
290     }
291 
292     @Test
293     public void testTransformHostComponentMustNotPerformKeyConversion() throws Exception {
294         File outputJarFile = runTransform();
295 
296         assertBeanNames(outputJarFile, "META-INF/spring/atlassian-plugins-host-components.xml",
297                 BEAN_NAMESPACE, BEAN_XPATH,
298                 Sets.newHashSet("TESTING5", "testing6", "testing7"));
299     }
300 
301     @Test
302     public void testHostComponentsMustBeSorted() throws Exception {
303         // Host components can be activated in any order, so should be sorted in output
304 
305         File outputJarFile = runTransform();
306 
307         List<String> actual = runXpath(outputJarFile, "META-INF/spring/atlassian-plugins-host-components.xml",
308                 BEAN_NAMESPACE, BEAN_XPATH);
309         List<String> expected = Arrays.asList("TESTING5", "testing6", "testing7");
310         assertThat(actual, is(expected));
311     }
312 
313     @Test
314     public void testDefaultTransformedJarIsNotCompressed() throws Exception {
315         // This test might be a bit brittle, but i think it's worth persisting with to see how it
316         // pans out. If you're here wondering why it failed, keep this in mind.
317         final File transformedFile = runTransform();
318         for (final JarEntry jarEntry : JarUtils.getEntries(transformedFile)) {
319             // It turns out uncompressed jar entries are actually still stored with method
320             // ZipEntry.STORED, so we can't check jarEntry.getMethod(). Also, you do need >= in the
321             // size check, because uncompressed things grow a little.
322             assertThat(jarEntry.getCompressedSize(), greaterThanOrEqualTo(jarEntry.getSize()));
323         }
324     }
325 
326     @Test
327     public void testBestSpeedCompressionYieldsCompressedTransformedJar() throws Exception {
328         compressionOptionYieldsCompressedTransformedJar(Deflater.BEST_SPEED);
329     }
330 
331     @Test
332     public void testBestSizeCompressionYieldsCompressedTransformedJar() throws Exception {
333         compressionOptionYieldsCompressedTransformedJar(Deflater.BEST_COMPRESSION);
334     }
335 
336     @Test
337     public void testMultipleTransformsGenerateSameOutput() throws Exception {
338         // Multiple transforms of the same plugin should generate exactly the same output file contents
339 
340         File outputA = runTransform();
341         File outputB = runTransform();
342 
343         // Both files should be the same length
344         assertThat(outputA.length(), is(outputB.length()));
345 
346         byte[] bytesA = Files.readAllBytes(Paths.get(outputA.getAbsolutePath()));
347         byte[] bytesB = Files.readAllBytes(Paths.get(outputB.getAbsolutePath()));
348 
349         // Both files should have the same contents
350         assertThat(bytesA, is(bytesB));
351     }
352 
353     public void compressionOptionYieldsCompressedTransformedJar(final int level) throws Exception {
354         // This test is slightly brittle, because it assumes a given file is improved by
355         // compression. However, an xml file is a good guess, and works with a reasonable margin.
356 
357         System.setProperty(DefaultPluginTransformer.TRANSFORM_COMPRESSION_LEVEL, Integer.toString(level));
358 
359         final JarEntry jarEntry = JarUtils.getEntry(runTransform(), PluginAccessor.Descriptor.FILENAME);
360         // It turns out uncompressed jar entries are actually still stored with method
361         // ZipEntry.STORED, so there's no value checking jarEntry.getMethod().
362         assertThat(jarEntry.getCompressedSize(), lessThan(jarEntry.getSize()));
363 
364         System.clearProperty(DefaultPluginTransformer.TRANSFORM_COMPRESSION_LEVEL);
365     }
366 
367     private File runTransform() throws Exception {
368         final File file = new PluginJarBuilder()
369                 .addFormattedJava("my.Foo",
370                         "package my;",
371                         "import com.atlassian.plugin.osgi.factory.transform.Fooable;",
372                         "import com.atlassian.plugin.osgi.factory.transform.FooChild;",
373                         "public class Foo {",
374                         "  private Fooable bar;",
375                         "  public Foo(Fooable bar, FooChild child) { this.bar = bar;} ",
376                         "}")
377                 .addFormattedJava("com.atlassian.plugin.osgi.SomeInterface",
378                         "package com.atlassian.plugin.osgi;",
379                         "public interface SomeInterface {}")
380                 .addFormattedResource("atlassian-plugin.xml",
381                         "<atlassian-plugin name='plugin1' key='first' pluginsVersion='2'>",
382                         "    <plugin-info>",
383                         "        <version>1.0</version>",
384                         "    </plugin-info>",
385                         "   <component key='testing3' class='my.Foo'/>",
386                         "   <component key='TESTING1' class='my.Foo'/>",
387                         "   <component key='testing2' class='my.Foo'/>",
388                         "   <component-import key='TESTING3'>",
389                         "       <interface>com.atlassian.plugin.osgi.SomeInterface</interface>",
390                         "   </component-import>",
391                         "   <component-import key='testing4'>",
392                         "       <interface>com.atlassian.plugin.osgi.SomeInterface</interface>",
393                         "   </component-import>",
394                         "</atlassian-plugin>")
395                 .build();
396 
397 
398         MockRegistration mockReg1 = new MockRegistration(new FooChild(), FooChild.class);
399         mockReg1.getProperties().put(PropertyBuilder.BEAN_NAME, "testing7");
400         MockRegistration mockReg2 = new MockRegistration(new Foo(), Fooable.class);
401         mockReg2.getProperties().put(PropertyBuilder.BEAN_NAME, "TESTING5");
402         MockRegistration mockReg3 = new MockRegistration(new FooChild(), FooChild.class);
403         mockReg3.getProperties().put(PropertyBuilder.BEAN_NAME, "testing6");
404 
405         return transformer.transform(new JarPluginArtifact(file), Arrays.<HostComponentRegistration>asList(mockReg1, mockReg2, mockReg3));
406     }
407 
408     private List<String> runXpath(File outputJarFile, String springFileLocation,
409                                   Map<String, String> namespaces, String xpathQuery) throws Exception {
410         List<String> foundBeans = new ArrayList<>();
411 
412         try (JarFile jarFile = new JarFile(outputJarFile)) {
413             InputStream inputStream = jarFile.getInputStream(jarFile.getEntry(springFileLocation));
414 
415             SAXReader saxReader = new SAXReader();
416             Document document = saxReader.read(inputStream);
417 
418             XPath xpath = new Dom4jXPath(xpathQuery);
419             xpath.setNamespaceContext(new SimpleNamespaceContext(namespaces));
420             List<Element> elems = xpath.selectNodes(document);
421             for (Element elem : elems) {
422                 foundBeans.add(elem.attribute("id").getValue());
423             }
424         }
425 
426         return foundBeans;
427     }
428 
429     private void assertBeanNames(File outputJarFile, String springFileLocation,
430                                  Map<String, String> namespaces, String xpathQuery,
431                                  Set<String> expectedIds) throws Exception {
432         Set<String> foundBeans = new HashSet<String>(runXpath(outputJarFile, springFileLocation, namespaces, xpathQuery));
433         assertThat(foundBeans, is(expectedIds));
434     }
435 }