View Javadoc

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