View Javadoc
1   package com.atlassian.plugin.test;
2   
3   import org.apache.commons.io.IOUtils;
4   import org.codehaus.commons.compiler.CompileException;
5   import org.codehaus.janino.ByteArrayClassLoader;
6   import org.codehaus.janino.Parser;
7   import org.codehaus.janino.SimpleCompiler;
8   
9   import java.io.File;
10  import java.io.FileInputStream;
11  import java.io.FileOutputStream;
12  import java.io.IOException;
13  import java.io.StringReader;
14  import java.lang.reflect.Field;
15  import java.util.HashMap;
16  import java.util.Map;
17  import java.util.zip.ZipEntry;
18  import java.util.zip.ZipOutputStream;
19  
20  /**
21   * Builds a plugin jar, including optionally compiling simple Java code
22   */
23  public class PluginJarBuilder {
24      private final Map<String, byte[]> jarContents;
25      private final String name;
26      private ClassLoader classLoader;
27  
28      /**
29       * Creates the builder
30       */
31      public PluginJarBuilder() {
32          this("test");
33      }
34  
35      /**
36       * Creates the builder
37       *
38       * @param name The plugin name
39       */
40      public PluginJarBuilder(String name) {
41          this(name, PluginJarBuilder.class.getClassLoader());
42      }
43  
44      /**
45       * Creates the builder
46       *
47       * @param name The plugin name
48       */
49      public PluginJarBuilder(String name, ClassLoader classLoader) {
50          jarContents = new HashMap<>();
51          this.name = name;
52          this.classLoader = classLoader;
53      }
54  
55      public PluginJarBuilder manifest(Map<String, String> manifest) {
56          StringBuilder sb = new StringBuilder();
57          for (Map.Entry<String, String> entry : manifest.entrySet()) {
58              sb.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n');
59          }
60          sb.append('\n');
61          return addResource("META-INF/MANIFEST.MF", sb.toString());
62      }
63  
64      public PluginJarBuilder addClass(Class<?> aClass) throws IOException {
65          jarContents.put(
66                  aClass.getName().replace('.', '/') + ".class",
67                  IOUtils.toByteArray(aClass.getResourceAsStream(aClass.getSimpleName() + ".class")));
68          return this;
69      }
70  
71      public PluginJarBuilder addFormattedJava(String className, String... lines) throws Exception {
72          StringBuilder sb = new StringBuilder();
73          for (String line : lines) {
74              sb.append(line.replace('\'', '"')).append('\n');
75          }
76          return addJava(className, sb.toString());
77      }
78  
79      /**
80       * Adds a Java class in source form. Will compile the source code.
81       *
82       * @param className The class name
83       * @param code      The code to compile
84       * @return The builder
85       */
86      public PluginJarBuilder addJava(String className, String code) throws Exception {
87          SimpleCompiler compiler = new SimpleCompiler();
88          compiler.setParentClassLoader(classLoader);
89          try {
90              compiler.cook(new StringReader(code));
91          } catch (CompileException ex) {
92              throw new IllegalArgumentException("Unable to compile " + className, ex);
93          }
94          classLoader = compiler.getClassLoader();
95  
96  
97          // jar files use '/' as a directory separator, regardless of platform
98          jarContents.put(className.replace('.', '/') + ".class", getClassFile(className));
99          return this;
100     }
101 
102     public byte[] getClassFile(String className) throws NoSuchFieldException, IllegalAccessException {
103         // Silly hack because I'm too lazy to do it the "proper" janino way
104         ByteArrayClassLoader cl = (ByteArrayClassLoader) classLoader;
105         Field field = cl.getClass().getDeclaredField("classes");
106         field.setAccessible(true);
107         Map classes = (Map) field.get(cl);
108 
109         return (byte[]) classes.get(className);
110     }
111 
112     /**
113      * Adds a resource in the jar from a string
114      *
115      * @param path     The path for the jar entry
116      * @param contents The contents of the file to create
117      */
118     public PluginJarBuilder addResource(String path, String contents) {
119         jarContents.put(path, contents.getBytes());
120         return this;
121     }
122 
123     /**
124      * Adds a resource in the jar as lines. Single quotes are converted to double quotes.
125      *
126      * @param path  The path for the jar entry
127      * @param lines The contents of the file to create
128      */
129     public PluginJarBuilder addFormattedResource(String path, String... lines) {
130         StringBuilder sb = new StringBuilder();
131         for (String line : lines)
132             sb.append(line.replace('\'', '"')).append('\n');
133         jarContents.put(path, sb.toString().getBytes());
134         return this;
135     }
136 
137     public PluginJarBuilder addPluginInformation(String key, String name, String version) {
138         return addPluginInformation(key, name, version, 2);
139     }
140 
141     public PluginJarBuilder addPluginInformation(String key, String name, String version, int pluginsVersion) {
142         return addPluginInformation(key, name, version, pluginsVersion, null);
143     }
144 
145     public PluginJarBuilder addPluginInformation(String key, String name, String version, int pluginsVersion, Map<String, String> params) {
146         StringBuilder sb = new StringBuilder();
147         sb.append("<atlassian-plugin name=\"").append(name).append("\" key=\"").append(key).append("\" pluginsVersion=\"").append(pluginsVersion).append("\">\n");
148         sb.append("    <plugin-info>\n");
149         sb.append("        <description>This plugin descriptor is used for testing plugins!</description>\n");
150         sb.append("        <version>").append(version).append("</version>\n");
151         sb.append("        <vendor name=\"Atlassian Software Systems Pty Ltd\" url=\"http://www.atlassian.com\" />\n");
152         if (params != null)
153             for (Map.Entry<String, String> param : params.entrySet())
154                 sb.append("<param name=\"").append(param.getKey()).append("\">").append(param.getValue()).append("</param>\n");
155         sb.append("    </plugin-info>");
156         sb.append("</atlassian-plugin>");
157         jarContents.put("atlassian-plugin.xml", sb.toString().getBytes());
158         return this;
159     }
160 
161     /**
162      * Adds a file to the jar
163      *
164      * @param path The path for the entry
165      * @param file The file to add
166      */
167     public PluginJarBuilder addFile(String path, File file) throws IOException {
168         FileInputStream in = new FileInputStream(file);
169         jarContents.put(path, IOUtils.toByteArray(in));
170         IOUtils.closeQuietly(in);
171         return this;
172     }
173 
174     /**
175      * Builds a jar file from the provided information. The file name is not guaranteed to match the jar name, as it is
176      * created as a temporary file.
177      *
178      * @return The created jar plugin
179      */
180     public File build() throws IOException {
181         return build(createBaseDir());
182     }
183 
184     private File createBaseDir() {
185         File baseDir = new File("target");
186         if (!baseDir.exists())
187             baseDir = new File(System.getProperty("java.io.tmpdir"));
188         else {
189             baseDir = new File(baseDir, "tmp");
190             if (!baseDir.exists())
191                 baseDir.mkdir();
192         }
193         return baseDir;
194     }
195 
196     public File buildWithNoManifest() throws IOException {
197         return build(createBaseDir(), false);
198     }
199 
200     /**
201      * Builds a jar file from the provided information. The file name is not guaranteed to match the jar name, as it is
202      * created as a temporary file.
203      *
204      * @param baseDir The base directory for generated plugin files
205      * @return The created jar plugin
206      */
207     public File build(File baseDir) throws IOException {
208         return build(baseDir, true);
209     }
210 
211     private File build(File baseDir, boolean createManifest) throws IOException {
212         // Ensure there is a manifest
213         if (createManifest && !jarContents.containsKey("META-INF/MANIFEST.MF")) {
214             jarContents.put("META-INF/MANIFEST.MF", "Manifest-Version: 1.0".getBytes());
215         }
216 
217         File jarFile = File.createTempFile(name, ".jar", baseDir);
218         try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(jarFile))) {
219             for (Map.Entry<String, byte[]> entry :jarContents.entrySet()) {
220                 zout.putNextEntry(new ZipEntry(entry.getKey()));
221                 zout.write(entry.getValue());
222             }
223         }
224         return jarFile;
225     }
226 
227     public ClassLoader getClassLoader() {
228         return classLoader;
229     }
230 
231 }