1 package com.atlassian.plugin.classloader;
2
3 import org.apache.commons.io.FileUtils;
4 import org.apache.commons.io.IOUtils;
5 import org.apache.commons.lang.Validate;
6 import org.codehaus.classworlds.uberjar.protocol.jar.NonLockingJarHandler;
7
8 import java.io.File;
9 import java.io.FileOutputStream;
10 import java.io.IOException;
11 import java.io.InputStream;
12 import java.net.MalformedURLException;
13 import java.net.URL;
14 import java.util.ArrayList;
15 import java.util.Enumeration;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.jar.JarEntry;
20 import java.util.jar.JarFile;
21
22
23
24
25 public final class PluginClassLoader extends ClassLoader
26 {
27 private static final String PLUGIN_INNER_JAR_PREFIX = "atlassian-plugins-innerjar";
28
29
30
31 private final List<File> pluginInnerJars;
32
33
34
35 private final Map<String, URL> entryMappings = new HashMap<String, URL>();
36
37
38
39 private final File tempDirectory;
40
41
42
43
44 public PluginClassLoader(final File pluginFile)
45 {
46 this(pluginFile, null);
47 }
48
49
50
51
52
53 public PluginClassLoader(final File pluginFile, final ClassLoader parent)
54 {
55 this(pluginFile, parent, new File(System.getProperty("java.io.tmpdir")));
56 }
57
58
59
60
61
62
63
64 public PluginClassLoader(final File pluginFile, final ClassLoader parent, final File tempDirectory)
65 {
66 super(parent);
67 Validate.isTrue(tempDirectory.exists(), "Temp directory should exist");
68 this.tempDirectory = tempDirectory;
69 try
70 {
71 if ((pluginFile == null) || !pluginFile.exists())
72 {
73 throw new IllegalArgumentException("Plugin jar file must not be null and must exist.");
74 }
75 pluginInnerJars = new ArrayList<File>();
76 initialiseOuterJar(pluginFile);
77 }
78 catch (final IOException e)
79 {
80 throw new IllegalStateException(e);
81 }
82 }
83
84
85
86
87
88
89
90
91 private void initialiseOuterJar(final File file) throws IOException
92 {
93 final JarFile jarFile = new JarFile(file);
94 try
95 {
96 for (final Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();)
97 {
98 final JarEntry jarEntry = entries.nextElement();
99 if (isInnerJarPath(jarEntry.getName()))
100 {
101 initialiseInnerJar(jarFile, jarEntry);
102 }
103 else
104 {
105 addEntryMapping(jarEntry, file, true);
106 }
107 }
108 }
109 finally
110 {
111 jarFile.close();
112 }
113 }
114
115 private boolean isInnerJarPath(final String name)
116 {
117 return name.startsWith("META-INF/lib/") && name.endsWith(".jar");
118 }
119
120 private void initialiseInnerJar(final JarFile jarFile, final JarEntry jarEntry) throws IOException
121 {
122 InputStream inputStream = null;
123 FileOutputStream fileOutputStream = null;
124 try
125 {
126 final File innerJarFile = File.createTempFile(PLUGIN_INNER_JAR_PREFIX, ".jar", tempDirectory);
127 inputStream = jarFile.getInputStream(jarEntry);
128 fileOutputStream = new FileOutputStream(innerJarFile);
129 IOUtils.copy(inputStream, fileOutputStream);
130 IOUtils.closeQuietly(fileOutputStream);
131
132 final JarFile innerJarJarFile = new JarFile(innerJarFile);
133 try
134 {
135 for (final Enumeration<JarEntry> entries = innerJarJarFile.entries(); entries.hasMoreElements();)
136 {
137 final JarEntry innerJarEntry = entries.nextElement();
138 addEntryMapping(innerJarEntry, innerJarFile, false);
139 }
140 }
141 finally
142 {
143 innerJarJarFile.close();
144 }
145
146 pluginInnerJars.add(innerJarFile);
147 }
148 finally
149 {
150 IOUtils.closeQuietly(inputStream);
151 IOUtils.closeQuietly(fileOutputStream);
152 }
153 }
154
155
156
157
158
159
160
161
162
163
164
165 @Override
166 protected synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException
167 {
168
169 final Class<?> c = findLoadedClass(name);
170 if (c != null)
171 {
172 return c;
173 }
174
175
176 final String path = name.replace('.', '/').concat(".class");
177 if (isEntryInPlugin(path))
178 {
179 try
180 {
181 return loadClassFromPlugin(name, path);
182 }
183 catch (final IOException e)
184 {
185 throw new ClassNotFoundException("Unable to load class [ " + name + " ] from PluginClassLoader", e);
186 }
187 }
188 return super.loadClass(name, resolve);
189 }
190
191
192
193
194
195
196
197
198 @Override
199 public URL getResource(final String name)
200 {
201 if (isEntryInPlugin(name))
202 {
203 return entryMappings.get(name);
204 }
205 else
206 {
207 return super.getResource(name);
208 }
209 }
210
211
212
213
214
215
216
217 public URL getLocalResource(final String name)
218 {
219 if (isEntryInPlugin(name))
220 {
221 return getResource(name);
222 }
223 else
224 {
225 return null;
226 }
227 }
228
229 public void close()
230 {
231 for (final File pluginInnerJar : pluginInnerJars)
232 {
233 FileUtils.deleteQuietly(pluginInnerJar);
234 }
235 }
236
237 List<File> getPluginInnerJars()
238 {
239 return new ArrayList<File>(pluginInnerJars);
240 }
241
242
243
244
245
246
247
248 private void initializePackage(final String className)
249 {
250 final int i = className.lastIndexOf('.');
251 if (i != -1)
252 {
253 final String pkgname = className.substring(0, i);
254
255 final Package pkg = getPackage(pkgname);
256 if (pkg == null)
257 {
258 definePackage(pkgname, null, null, null, null, null, null, null);
259 }
260 }
261 }
262
263 private Class<?> loadClassFromPlugin(final String className, final String path) throws IOException
264 {
265 InputStream inputStream = null;
266 try
267 {
268 final URL resourceURL = entryMappings.get(path);
269 inputStream = resourceURL.openStream();
270 final byte[] bytez = IOUtils.toByteArray(inputStream);
271 initializePackage(className);
272 return defineClass(className, bytez, 0, bytez.length);
273 }
274 finally
275 {
276 IOUtils.closeQuietly(inputStream);
277 }
278 }
279
280 private URL getUrlOfResourceInJar(final String name, final File jarFile)
281 {
282 try
283 {
284 return new URL(new URL("jar:file:" + jarFile.getAbsolutePath() + "!/"), name, NonLockingJarHandler.getInstance());
285 }
286 catch (final MalformedURLException e)
287 {
288 throw new RuntimeException(e);
289 }
290 }
291
292 private boolean isEntryInPlugin(final String name)
293 {
294 return entryMappings.containsKey(name);
295 }
296
297 private void addEntryMapping(final JarEntry jarEntry, final File jarFile, final boolean overrideExistingEntries)
298 {
299 if (overrideExistingEntries)
300 {
301 addEntryUrl(jarEntry, jarFile);
302 }
303 else
304 {
305 if (!entryMappings.containsKey(jarEntry.getName()))
306 {
307 addEntryUrl(jarEntry, jarFile);
308 }
309 }
310 }
311
312 private void addEntryUrl(final JarEntry jarEntry, final File jarFile)
313 {
314 entryMappings.put(jarEntry.getName(), getUrlOfResourceInJar(jarEntry.getName(), jarFile));
315 }
316 }